Merge pull request #1653 from lightpanda-io/dummy-performance-timing

add dummy PerformanceTiming
This commit is contained in:
Karl Seguin
2026-02-26 06:50:19 +08:00
committed by GitHub
2 changed files with 170 additions and 1 deletions

View File

@@ -274,6 +274,50 @@
} }
</script> </script>
<script id=performance_timing_exists>
{
// performance.timing must not be undefined (used by sites like Bing)
testing.expectEqual(true, performance.timing !== undefined);
testing.expectEqual(true, performance.timing !== null);
}
</script>
<script id=performance_timing_navigationStart>
{
// navigationStart must be a number (sites access performance.timing.navigationStart)
const timing = performance.timing;
testing.expectEqual('number', typeof timing.navigationStart);
testing.expectEqual(0, timing.navigationStart);
}
</script>
<script id=performance_timing_all_properties>
{
// All PerformanceTiming properties must be accessible and return numbers
const timing = performance.timing;
const props = [
'navigationStart', 'unloadEventStart', 'unloadEventEnd',
'redirectStart', 'redirectEnd', 'fetchStart',
'domainLookupStart', 'domainLookupEnd',
'connectStart', 'connectEnd', 'secureConnectionStart',
'requestStart', 'responseStart', 'responseEnd',
'domLoading', 'domInteractive',
'domContentLoadedEventStart', 'domContentLoadedEventEnd',
'domComplete', 'loadEventStart', 'loadEventEnd',
];
for (const prop of props) {
testing.expectEqual('number', typeof timing[prop]);
}
}
</script>
<script id=performance_timing_same_object>
{
// performance.timing should return the same object on each access
testing.expectEqual(true, performance.timing === performance.timing);
}
</script>
<script id=mixed_marks_and_measures> <script id=mixed_marks_and_measures>
{ {
performance.clearMarks(); performance.clearMarks();
@@ -302,3 +346,50 @@
testing.expectEqual(0, remainingMarks.length); testing.expectEqual(0, remainingMarks.length);
} }
</script> </script>
<script id=performance_timing_exists>
{
// Navigation Timing Level 1: performance.timing must be an object, not undefined
testing.expectEqual('object', typeof performance.timing);
testing.expectEqual(false, performance.timing === null);
}
</script>
<script id=performance_timing_navigationStart>
{
// The most commonly used property — must be a number (not undefined)
testing.expectEqual('number', typeof performance.timing.navigationStart);
testing.expectEqual(0, performance.timing.navigationStart);
}
</script>
<script id=performance_navigation_exists>
{
// Navigation Timing Level 1: performance.navigation must be an object, not undefined
testing.expectEqual('object', typeof performance.navigation);
testing.expectEqual(false, performance.navigation === null);
testing.expectEqual('number', typeof performance.navigation.type);
testing.expectEqual('number', typeof performance.navigation.redirectCount);
testing.expectEqual(0, performance.navigation.type);
testing.expectEqual(0, performance.navigation.redirectCount);
}
</script>
<script id=performance_timing_all_properties>
{
const t = performance.timing;
const props = [
'navigationStart', 'unloadEventStart', 'unloadEventEnd',
'redirectStart', 'redirectEnd', 'fetchStart',
'domainLookupStart', 'domainLookupEnd',
'connectStart', 'connectEnd', 'secureConnectionStart',
'requestStart', 'responseStart', 'responseEnd',
'domLoading', 'domInteractive',
'domContentLoadedEventStart', 'domContentLoadedEventEnd',
'domComplete', 'loadEventStart', 'loadEventEnd',
];
for (const prop of props) {
testing.expectEqual('number', typeof t[prop]);
}
}
</script>

View File

@@ -3,7 +3,7 @@ const Page = @import("../Page.zig");
const datetime = @import("../../datetime.zig"); const datetime = @import("../../datetime.zig");
pub fn registerTypes() []const type { pub fn registerTypes() []const type {
return &.{ Performance, Entry, Mark, Measure }; return &.{ Performance, Entry, Mark, Measure, PerformanceTiming, PerformanceNavigation };
} }
const std = @import("std"); const std = @import("std");
@@ -12,6 +12,8 @@ const Performance = @This();
_time_origin: u64, _time_origin: u64,
_entries: std.ArrayList(*Entry) = .{}, _entries: std.ArrayList(*Entry) = .{},
_timing: PerformanceTiming = .{},
_navigation: PerformanceNavigation = .{},
/// Get high-resolution timestamp in microseconds, rounded to 5μs increments /// Get high-resolution timestamp in microseconds, rounded to 5μs increments
/// to match browser behavior (prevents fingerprinting) /// to match browser behavior (prevents fingerprinting)
@@ -27,9 +29,15 @@ pub fn init() Performance {
return .{ return .{
._time_origin = highResTimestamp(), ._time_origin = highResTimestamp(),
._entries = .{}, ._entries = .{},
._timing = .{},
._navigation = .{},
}; };
} }
pub fn getTiming(self: *Performance) *PerformanceTiming {
return &self._timing;
}
pub fn now(self: *const Performance) f64 { pub fn now(self: *const Performance) f64 {
const current = highResTimestamp(); const current = highResTimestamp();
const elapsed = current - self._time_origin; const elapsed = current - self._time_origin;
@@ -42,6 +50,10 @@ pub fn getTimeOrigin(self: *const Performance) f64 {
return @as(f64, @floatFromInt(self._time_origin)) / 1000.0; return @as(f64, @floatFromInt(self._time_origin)) / 1000.0;
} }
pub fn getNavigation(self: *Performance) *PerformanceNavigation {
return &self._navigation;
}
pub fn mark( pub fn mark(
self: *Performance, self: *Performance,
name: []const u8, name: []const u8,
@@ -263,6 +275,8 @@ pub const JsApi = struct {
pub const getEntriesByType = bridge.function(Performance.getEntriesByType, .{}); pub const getEntriesByType = bridge.function(Performance.getEntriesByType, .{});
pub const getEntriesByName = bridge.function(Performance.getEntriesByName, .{}); pub const getEntriesByName = bridge.function(Performance.getEntriesByName, .{});
pub const timeOrigin = bridge.accessor(Performance.getTimeOrigin, null, .{}); pub const timeOrigin = bridge.accessor(Performance.getTimeOrigin, null, .{});
pub const timing = bridge.accessor(Performance.getTiming, null, .{});
pub const navigation = bridge.accessor(Performance.getNavigation, null, .{});
}; };
pub const Entry = struct { pub const Entry = struct {
@@ -449,6 +463,70 @@ pub const Measure = struct {
}; };
}; };
/// PerformanceTiming — Navigation Timing Level 1 (legacy, but widely used).
/// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceTiming
/// All properties return 0 as stub values; the object must not be undefined
/// so that scripts accessing performance.timing.navigationStart don't crash.
pub const PerformanceTiming = struct {
// Padding to avoid zero-size struct, which causes identity_map pointer collisions.
_pad: bool = false,
pub const JsApi = struct {
pub const bridge = js.Bridge(PerformanceTiming);
pub const Meta = struct {
pub const name = "PerformanceTiming";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const empty_with_no_proto = true;
};
pub const navigationStart = bridge.property(0.0, .{ .template = false, .readonly = true });
pub const unloadEventStart = bridge.property(0.0, .{ .template = false, .readonly = true });
pub const unloadEventEnd = bridge.property(0.0, .{ .template = false, .readonly = true });
pub const redirectStart = bridge.property(0.0, .{ .template = false, .readonly = true });
pub const redirectEnd = bridge.property(0.0, .{ .template = false, .readonly = true });
pub const fetchStart = bridge.property(0.0, .{ .template = false, .readonly = true });
pub const domainLookupStart = bridge.property(0.0, .{ .template = false, .readonly = true });
pub const domainLookupEnd = bridge.property(0.0, .{ .template = false, .readonly = true });
pub const connectStart = bridge.property(0.0, .{ .template = false, .readonly = true });
pub const connectEnd = bridge.property(0.0, .{ .template = false, .readonly = true });
pub const secureConnectionStart = bridge.property(0.0, .{ .template = false, .readonly = true });
pub const requestStart = bridge.property(0.0, .{ .template = false, .readonly = true });
pub const responseStart = bridge.property(0.0, .{ .template = false, .readonly = true });
pub const responseEnd = bridge.property(0.0, .{ .template = false, .readonly = true });
pub const domLoading = bridge.property(0.0, .{ .template = false, .readonly = true });
pub const domInteractive = bridge.property(0.0, .{ .template = false, .readonly = true });
pub const domContentLoadedEventStart = bridge.property(0.0, .{ .template = false, .readonly = true });
pub const domContentLoadedEventEnd = bridge.property(0.0, .{ .template = false, .readonly = true });
pub const domComplete = bridge.property(0.0, .{ .template = false, .readonly = true });
pub const loadEventStart = bridge.property(0.0, .{ .template = false, .readonly = true });
pub const loadEventEnd = bridge.property(0.0, .{ .template = false, .readonly = true });
};
};
// PerformanceNavigation implements the Navigation Timing Level 1 API.
// https://www.w3.org/TR/navigation-timing/#sec-navigation-navigation-timing-interface
// Stub implementation — returns 0 for type (TYPE_NAVIGATE) and 0 for redirectCount.
pub const PerformanceNavigation = struct {
// Padding to avoid zero-size struct, which causes identity_map pointer collisions.
_pad: bool = false,
pub const JsApi = struct {
pub const bridge = js.Bridge(PerformanceNavigation);
pub const Meta = struct {
pub const name = "PerformanceNavigation";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const empty_with_no_proto = true;
};
pub const @"type" = bridge.property(0.0, .{ .template = false, .readonly = true });
pub const redirectCount = bridge.property(0.0, .{ .template = false, .readonly = true });
};
};
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");
test "WebApi: Performance" { test "WebApi: Performance" {
try testing.htmlRunner("performance.html", .{}); try testing.htmlRunner("performance.html", .{});