diff --git a/Performance/index.js b/Performance/index.js new file mode 100644 index 0000000..36c5f9a --- /dev/null +++ b/Performance/index.js @@ -0,0 +1,878 @@ +/** + * WHATWG Performance: hr-time (performance.now/timeOrigin), User Timing + * (mark/measure), the Performance Timeline queries and PerformanceObserver, + * plus the same surface inside Worker isolates. + * + * Deliberate deviations from the specifications asserted here: + * - Observer callbacks are only guaranteed to run asynchronously; nothing + * pins them to a microtask or a macrotask turn. + * - `detail` is structured-cloned per spec where the runtime ships + * structuredClone, and degrades to by-reference where it does not (a + * runtime may implement the Performance API first); the detail specs + * assert whichever contract applies. + */ + +var globalObject = typeof globalThis !== "undefined" ? globalThis : global; + +// The suite gates itself on the API being implemented so it can run from +// runAllTests() on every runtime: one that has not shipped the Performance API +// reports a single pending spec instead of failures. Runtimes that do ship it +// must keep an unguarded canary in their own test suite asserting the globals +// exist, so this gate cannot silently hide a regression there. +if (typeof globalObject.performance === "undefined" || typeof globalObject.PerformanceObserver !== "function") { + describe("Performance API", function () { + it("is skipped: this runtime does not implement the Performance API", function () { + pending(); + }); + }); + return; +} + +var HAS_STRUCTURED_CLONE = typeof globalObject.structuredClone === "function"; + +var PERFORMANCE_CONSTRUCTORS = [ + "Performance", + "PerformanceEntry", + "PerformanceMark", + "PerformanceMeasure", + "PerformanceObserver", + "PerformanceObserverEntryList" +]; + +function captureThrown(fn) { + try { + fn(); + } catch (e) { + return e; + } + return null; +} + +function expectThrowsNamed(fn, name) { + var thrown = captureThrown(fn); + expect(thrown).not.toBeNull(); + expect(thrown && thrown.name).toBe(name); + return thrown; +} + +function expectThrowsTypeError(fn) { + var thrown = expectThrowsNamed(fn, "TypeError"); + expect(thrown instanceof TypeError).toBe(true); + return thrown; +} + +function clearTimeline() { + performance.clearMarks(); + performance.clearMeasures(); +} + +function entryNames(entries) { + var names = []; + for (var i = 0; i < entries.length; i++) { + names.push(entries[i].name); + } + return names; +} + +// A callback that throws is routed to reportError, which surfaces as a global +// `error` event. preventDefault() keeps it from reaching the runner's uncaught +// handler for the one test that exercises that path. +function suppressGlobalErrors() { + if (typeof globalObject.addEventListener !== "function") { + return function () {}; + } + var listener = function (event) { + if (event && typeof event.preventDefault === "function") { + event.preventDefault(); + } + }; + globalObject.addEventListener("error", listener); + return function () { + if (typeof globalObject.removeEventListener === "function") { + globalObject.removeEventListener("error", listener); + } + }; +} + +describe("Performance globals", function () { + beforeEach(clearTimeline); + + it("Should expose performance and every performance constructor", function () { + expect(typeof performance).toBe("object"); + var types = []; + for (var i = 0; i < PERFORMANCE_CONSTRUCTORS.length; i++) { + types.push(PERFORMANCE_CONSTRUCTORS[i] + ": " + typeof globalObject[PERFORMANCE_CONSTRUCTORS[i]]); + } + expect(types).toEqual(PERFORMANCE_CONSTRUCTORS.map(function (name) { + return name + ": function"; + })); + }); + + it("Should install them as own writable, enumerable, configurable properties", function () { + var names = PERFORMANCE_CONSTRUCTORS.concat(["performance"]); + var actual = names.map(function (name) { + var descriptor = Object.getOwnPropertyDescriptor(globalObject, name); + if (!descriptor) { + return name + ": missing"; + } + return name + ": " + [descriptor.writable, descriptor.enumerable, descriptor.configurable].join("/"); + }); + expect(actual).toEqual(names.map(function (name) { + return name + ": true/true/true"; + })); + }); + + it("Should make Performance an EventTarget subclass", function () { + expect(performance instanceof Performance).toBe(true); + expect(performance instanceof EventTarget).toBe(true); + expect(Object.getPrototypeOf(Performance.prototype)).toBe(EventTarget.prototype); + }); + + it("Should reject illegal constructors", function () { + var illegal = [Performance, PerformanceEntry, PerformanceMeasure, PerformanceObserverEntryList]; + for (var i = 0; i < illegal.length; i++) { + var thrown = expectThrowsTypeError((function (ctor) { + return function () { new ctor(); }; + })(illegal[i])); + expect(thrown && thrown.message).toContain("Illegal constructor"); + } + }); + + it("Should let PerformanceMark be constructed directly", function () { + var mark = new PerformanceMark("constructed"); + expect(mark instanceof PerformanceMark).toBe(true); + expect(mark instanceof PerformanceEntry).toBe(true); + expect(mark.name).toBe("constructed"); + expect(mark.entryType).toBe("mark"); + expect(mark.duration).toBe(0); + }); + + it("Should brand instances with Symbol.toStringTag", function () { + var mark = performance.mark("tagged"); + var measure = performance.measure("tagged", { start: 0, end: 1 }); + expect(Object.prototype.toString.call(performance)).toBe("[object Performance]"); + expect(Object.prototype.toString.call(mark)).toBe("[object PerformanceMark]"); + expect(Object.prototype.toString.call(measure)).toBe("[object PerformanceMeasure]"); + }); + + it("Should keep Symbol.toStringTag configurable and non writable", function () { + var prototypes = [Performance, PerformanceMark, PerformanceMeasure, PerformanceObserverEntryList]; + var actual = prototypes.map(function (ctor) { + var descriptor = Object.getOwnPropertyDescriptor(ctor.prototype, Symbol.toStringTag); + if (!descriptor) { + return "missing"; + } + return descriptor.value + ": " + descriptor.writable + "/" + descriptor.configurable; + }); + expect(actual).toEqual([ + "Performance: false/true", + "PerformanceMark: false/true", + "PerformanceMeasure: false/true", + "PerformanceObserverEntryList: false/true" + ]); + }); + + it("Should hide the EventTarget bookkeeping from Object.keys", function () { + expect(Object.keys(performance).indexOf("_listeners")).toBe(-1); + }); + + it("Should inherit working EventTarget methods", function () { + expect(typeof performance.addEventListener).toBe("function"); + expect(typeof performance.removeEventListener).toBe("function"); + expect(typeof performance.dispatchEvent).toBe("function"); + + var received = null; + var listener = function (event) { received = event; }; + performance.addEventListener("nstest", listener); + performance.dispatchEvent(new Event("nstest")); + performance.removeEventListener("nstest", listener); + expect(received).not.toBeNull(); + expect(received && received.type).toBe("nstest"); + + received = null; + performance.dispatchEvent(new Event("nstest")); + expect(received).toBeNull(); + }); +}); + +describe("Performance high resolution time", function () { + it("Should return a finite number of milliseconds that never goes backwards", function () { + var previous = performance.now(); + expect(typeof previous).toBe("number"); + expect(isFinite(previous)).toBe(true); + expect(previous).toBeGreaterThan(0); + + var regressions = 0; + for (var i = 0; i < 200; i++) { + var current = performance.now(); + if (current < previous) { + regressions++; + } + previous = current; + } + expect(regressions).toBe(0); + }); + + it("Should not be coarsened to whole milliseconds", function () { + var fractional = 0; + for (var i = 0; i < 50; i++) { + var sample = performance.now(); + if (sample !== Math.floor(sample)) { + fractional++; + } + } + expect(fractional).toBeGreaterThan(0); + }); + + it("Should expose timeOrigin as wall clock milliseconds since the epoch", function () { + expect(typeof performance.timeOrigin).toBe("number"); + expect(performance.timeOrigin).toBeGreaterThan(0); + // Reconstructing Date.now() from timeOrigin + now() races three clock + // reads in one expression; a scheduler or GC stall between them shows + // up as tens of ms of apparent offset on a contended CI host. A stall + // does not repeat across every sample, so the minimum over a few + // samples is the honest measurement, while a genuine anchoring or + // unit error persists through all of them — the bound only needs to + // catch those. + var minOffset = Infinity; + for (var i = 0; i < 10; i++) { + var offset = Math.abs(Date.now() - (performance.timeOrigin + performance.now())); + if (offset < minOffset) { + minOffset = offset; + } + } + expect(minOffset).toBeLessThan(250); + }); + + it("Should keep timeOrigin a readonly accessor on the prototype", function () { + var descriptor = Object.getOwnPropertyDescriptor(Performance.prototype, "timeOrigin"); + expect(descriptor).not.toBeUndefined(); + expect(typeof (descriptor && descriptor.get)).toBe("function"); + expect(descriptor && descriptor.set).toBeUndefined(); + expect(Object.prototype.hasOwnProperty.call(performance, "timeOrigin")).toBe(false); + + var before = performance.timeOrigin; + performance.timeOrigin = 0; + expect(performance.timeOrigin).toBe(before); + expect(Object.prototype.hasOwnProperty.call(performance, "timeOrigin")).toBe(false); + }); + + it("Should serialize to the time origin alone", function () { + var json = performance.toJSON(); + expect(Object.keys(json)).toEqual(["timeOrigin"]); + expect(json.timeOrigin).toBe(performance.timeOrigin); + }); +}); + +describe("Performance mark", function () { + beforeEach(clearTimeline); + + it("Should return a buffered mark stamped with the current time", function () { + var before = performance.now(); + var mark = performance.mark("m1"); + var after = performance.now(); + + expect(mark instanceof PerformanceMark).toBe(true); + expect(mark instanceof PerformanceEntry).toBe(true); + expect(mark.name).toBe("m1"); + expect(mark.entryType).toBe("mark"); + expect(mark.duration).toBe(0); + expect(mark.startTime >= before && mark.startTime <= after).toBe(true); + expect(performance.getEntriesByName("m1")[0]).toBe(mark); + expect(performance.getEntriesByType("mark")[0]).toBe(mark); + expect(performance.getEntries().length).toBe(1); + }); + + it("Should honour an explicit startTime", function () { + var mark = performance.mark("m2", { startTime: 12.5 }); + expect(mark.startTime).toBe(12.5); + expect(performance.getEntriesByName("m2")[0].startTime).toBe(12.5); + }); + + it("Should default detail to null and snapshot a supplied detail", function () { + expect(performance.mark("m3").detail).toBeNull(); + + var detail = { nested: { value: 1 } }; + var mark = performance.mark("m4", { detail: detail }); + expect(mark.detail).toEqual(detail); + expect(performance.getEntriesByName("m4")[0].detail).toBe(mark.detail); + if (HAS_STRUCTURED_CLONE) { + // Cloned once at creation: a distinct snapshot, immune to later + // mutation of the caller's object. + expect(mark.detail).not.toBe(detail); + detail.nested.value = 2; + expect(mark.detail.nested.value).toBe(1); + } else { + // Portability fallback for a runtime that ships the Performance + // API before structuredClone: detail degrades to by-reference. + expect(mark.detail).toBe(detail); + } + }); + + if (HAS_STRUCTURED_CLONE) { + it("Should reject an uncloneable detail with a DataCloneError named error", function () { + expectThrowsNamed(function () { + performance.mark("bad-detail", { detail: { fn: function () {} } }); + }, "DataCloneError"); + expect(performance.getEntries().length).toBe(0); + }); + } + + it("Should reject a negative or non finite startTime", function () { + var invalid = [-1, -0.5, NaN, Infinity, -Infinity]; + for (var i = 0; i < invalid.length; i++) { + expectThrowsTypeError((function (startTime) { + return function () { performance.mark("bad", { startTime: startTime }); }; + })(invalid[i])); + } + expect(performance.getEntries().length).toBe(0); + }); + + it("Should require a name", function () { + expectThrowsTypeError(function () { performance.mark(); }); + expect(performance.getEntries().length).toBe(0); + }); + + it("Should not buffer marks built with the PerformanceMark constructor", function () { + var detail = { standalone: true }; + var mark = new PerformanceMark("standalone", { startTime: 7, detail: detail }); + expect(mark.startTime).toBe(7); + expect(mark.detail).toEqual(detail); + expect(performance.getEntriesByName("standalone")).toEqual([]); + expect(performance.getEntries()).toEqual([]); + }); + + it("Should serialize the entry fields with toJSON", function () { + var detail = { any: "value" }; + var mark = performance.mark("m5", { startTime: 3, detail: detail }); + var json = mark.toJSON(); + expect(json.name).toBe("m5"); + expect(json.entryType).toBe("mark"); + expect(json.startTime).toBe(3); + expect(json.duration).toBe(0); + expect(json.detail).toBe(mark.detail); + }); +}); + +describe("Performance measure", function () { + beforeEach(clearTimeline); + + it("Should span the time origin to now when given only a name", function () { + var measure = performance.measure("m"); + expect(measure instanceof PerformanceMeasure).toBe(true); + expect(measure instanceof PerformanceEntry).toBe(true); + expect(measure.name).toBe("m"); + expect(measure.entryType).toBe("measure"); + expect(measure.startTime).toBe(0); + expect(measure.duration).toBeGreaterThan(0); + expect(measure.duration).not.toBeGreaterThan(performance.now()); + expect(measure.detail).toBeNull(); + expect(performance.getEntriesByName("m")[0]).toBe(measure); + }); + + it("Should span two marks", function () { + var start = performance.mark("a", { startTime: 10 }); + var end = performance.mark("b", { startTime: 40 }); + var measure = performance.measure("ab", "a", "b"); + expect(measure.startTime).toBe(start.startTime); + expect(measure.duration).toBe(end.startTime - start.startTime); + }); + + it("Should use the most recent mark of a repeated name", function () { + performance.mark("dup", { startTime: 10 }); + performance.mark("dup", { startTime: 50 }); + performance.mark("end", { startTime: 80 }); + var measure = performance.measure("m", "dup", "end"); + expect(measure.startTime).toBe(50); + expect(measure.duration).toBe(30); + }); + + it("Should measure up to now when only a start mark is given", function () { + performance.mark("start", { startTime: 5 }); + var measure = performance.measure("m", "start"); + expect(measure.startTime).toBe(5); + expect(measure.duration).toBeGreaterThan(0); + }); + + it("Should throw a SyntaxError named error for an unknown mark", function () { + performance.mark("known", { startTime: 1 }); + expectThrowsNamed(function () { performance.measure("m", "missing"); }, "SyntaxError"); + expectThrowsNamed(function () { performance.measure("m", "known", "missing"); }, "SyntaxError"); + expectThrowsNamed(function () { performance.measure("m", { start: "missing" }); }, "SyntaxError"); + expect(performance.getEntriesByType("measure")).toEqual([]); + }); + + it("Should accept the numeric options form", function () { + var startEnd = performance.measure("start-end", { start: 10, end: 40 }); + expect(startEnd.startTime).toBe(10); + expect(startEnd.duration).toBe(30); + + var startDuration = performance.measure("start-duration", { start: 10, duration: 5 }); + expect(startDuration.startTime).toBe(10); + expect(startDuration.duration).toBe(5); + + var durationEnd = performance.measure("duration-end", { duration: 5, end: 40 }); + expect(durationEnd.startTime).toBe(35); + expect(durationEnd.duration).toBe(5); + }); + + it("Should fill in the missing endpoint when only start or only end is given", function () { + var onlyStart = performance.measure("only-start", { start: 10 }); + expect(onlyStart.startTime).toBe(10); + expect(onlyStart.duration).toBeGreaterThan(0); + + var onlyEnd = performance.measure("only-end", { end: 40 }); + expect(onlyEnd.startTime).toBe(0); + expect(onlyEnd.duration).toBe(40); + }); + + it("Should accept mark names inside the options bag", function () { + performance.mark("a", { startTime: 10 }); + performance.mark("b", { startTime: 40 }); + var measure = performance.measure("m", { start: "a", end: "b" }); + expect(measure.startTime).toBe(10); + expect(measure.duration).toBe(30); + }); + + it("Should default detail to null and snapshot a supplied detail", function () { + var detail = { nested: { value: 1 } }; + var measure = performance.measure("detailed", { start: 0, end: 1, detail: detail }); + expect(measure.detail).toEqual(detail); + expect(performance.getEntriesByName("detailed")[0].detail).toBe(measure.detail); + expect(performance.measure("plain", { start: 0, end: 1 }).detail).toBeNull(); + if (HAS_STRUCTURED_CLONE) { + expect(measure.detail).not.toBe(detail); + detail.nested.value = 2; + expect(measure.detail.nested.value).toBe(1); + } else { + expect(measure.detail).toBe(detail); + } + }); + + it("Should treat an options bag without members like no options at all", function () { + // The options branch of measure() only engages when start, end, + // duration or detail is present; a bare {} is a boundless measure. + var boundless = performance.measure("boundless", {}); + expect(boundless.startTime).toBe(0); + expect(boundless.duration).toBeGreaterThan(0); + }); + + it("Should reject invalid option combinations", function () { + expectThrowsTypeError(function () { performance.measure("m", { start: 1, end: 2, duration: 1 }); }); + expectThrowsTypeError(function () { performance.measure("m", { detail: { onlyDetail: true } }); }); + expectThrowsTypeError(function () { performance.measure("m", { start: 1 }, "someMark"); }); + expect(performance.getEntriesByType("measure")).toEqual([]); + }); + + it("Should reject negative or non finite numeric endpoints", function () { + // duration is not converted through "convert a mark to a timestamp", + // so a negative duration is legal (it yields an end before the start); + // only non-finite values are rejected by the double conversion. + var invalid = [ + { start: -1 }, + { end: -1 }, + { start: NaN }, + { end: Infinity }, + { start: 0, duration: NaN } + ]; + for (var i = 0; i < invalid.length; i++) { + expectThrowsTypeError((function (options) { + return function () { performance.measure("m", options); }; + })(invalid[i])); + } + expect(performance.getEntriesByType("measure")).toEqual([]); + }); +}); + +describe("Performance timeline queries", function () { + beforeEach(clearTimeline); + + it("Should return a fresh array from every query", function () { + performance.mark("a"); + var first = performance.getEntries(); + var second = performance.getEntries(); + expect(first).not.toBe(second); + expect(first).toEqual(second); + + first.length = 0; + expect(performance.getEntries().length).toBe(1); + expect(performance.getEntriesByType("mark")).not.toBe(performance.getEntriesByType("mark")); + expect(performance.getEntriesByName("a")).not.toBe(performance.getEntriesByName("a")); + }); + + it("Should sort by startTime and keep insertion order for ties", function () { + var late = performance.mark("late", { startTime: 30 }); + var early = performance.mark("early", { startTime: 10 }); + var tieFirst = performance.mark("tie-first", { startTime: 20 }); + var tieSecond = performance.mark("tie-second", { startTime: 20 }); + expect(performance.getEntries()).toEqual([early, tieFirst, tieSecond, late]); + }); + + it("Should place a measure that starts earlier ahead of already buffered marks", function () { + var mark = performance.mark("later-mark", { startTime: 50 }); + var measure = performance.measure("earlier-measure", { start: 10, end: 20 }); + var entries = performance.getEntries(); + expect(entries.length).toBe(2); + expect(entries[0]).toBe(measure); + expect(entries[1]).toBe(mark); + }); + + it("Should filter by type and by name", function () { + var mark = performance.mark("shared", { startTime: 10 }); + var measure = performance.measure("shared", { start: 0, end: 5 }); + performance.mark("other", { startTime: 1 }); + + expect(entryNames(performance.getEntriesByType("mark"))).toEqual(["other", "shared"]); + expect(performance.getEntriesByType("measure")).toEqual([measure]); + expect(performance.getEntriesByType("navigation")).toEqual([]); + expect(performance.getEntriesByName("shared")).toEqual([measure, mark]); + expect(performance.getEntriesByName("shared", "mark")).toEqual([mark]); + expect(performance.getEntriesByName("shared", "measure")).toEqual([measure]); + expect(performance.getEntriesByName("nothing")).toEqual([]); + }); + + it("Should clear marks and measures independently", function () { + performance.mark("a"); + performance.mark("b"); + performance.measure("m", { start: 0, end: 1 }); + + performance.clearMarks(); + expect(performance.getEntriesByType("mark")).toEqual([]); + expect(performance.getEntriesByType("measure").length).toBe(1); + + performance.clearMeasures(); + expect(performance.getEntries()).toEqual([]); + }); + + it("Should clear only the named entries of that type when a name is given", function () { + performance.mark("keep", { startTime: 1 }); + performance.mark("drop", { startTime: 2 }); + performance.measure("keep", { start: 0, end: 1 }); + performance.measure("drop", { start: 0, end: 2 }); + + performance.clearMarks("drop"); + expect(entryNames(performance.getEntriesByType("mark"))).toEqual(["keep"]); + expect(entryNames(performance.getEntriesByType("measure"))).toEqual(["keep", "drop"]); + + performance.clearMeasures("drop"); + expect(entryNames(performance.getEntriesByType("measure"))).toEqual(["keep"]); + expect(entryNames(performance.getEntriesByType("mark"))).toEqual(["keep"]); + }); +}); + +describe("PerformanceObserver", function () { + var originalTimeout; + var observers; + + beforeEach(function () { + originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 8000; + observers = []; + clearTimeline(); + }); + + afterEach(function () { + for (var i = 0; i < observers.length; i++) { + observers[i].disconnect(); + } + observers = []; + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; + }); + + function observing(callback) { + var observer = new PerformanceObserver(callback); + observers.push(observer); + return observer; + } + + it("Should require a callable callback", function () { + expectThrowsTypeError(function () { new PerformanceObserver(); }); + expectThrowsTypeError(function () { new PerformanceObserver(null); }); + expectThrowsTypeError(function () { new PerformanceObserver({}); }); + expectThrowsTypeError(function () { new PerformanceObserver("mark"); }); + }); + + it("Should advertise the supported entry types as a frozen list", function () { + expect(PerformanceObserver.supportedEntryTypes).toEqual(["mark", "measure"]); + expect(Object.isFrozen(PerformanceObserver.supportedEntryTypes)).toBe(true); + }); + + it("Should reject malformed observe options", function () { + var observer = observing(function () {}); + expectThrowsTypeError(function () { observer.observe({ entryTypes: ["mark"], type: "mark" }); }); + expectThrowsTypeError(function () { observer.observe({ entryTypes: ["mark"], buffered: true }); }); + expectThrowsTypeError(function () { observer.observe({}); }); + expectThrowsTypeError(function () { observer.observe(); }); + }); + + it("Should convert entryTypes as a WebIDL sequence", function (done) { + // Non-iterables (and string primitives, which fail the object check) + // must throw rather than silently observe nothing. + var observer = observing(function () {}); + expectThrowsTypeError(function () { observer.observe({ entryTypes: 5 }); }); + expectThrowsTypeError(function () { observer.observe({ entryTypes: "mark" }); }); + expectThrowsTypeError(function () { observer.observe({ entryTypes: { length: 1, 0: "mark" } }); }); + + // Any iterable converts, not just arrays. + var fromSet = observing(function (list) { + expect(entryNames(list.getEntries())).toEqual(["set-observed"]); + done(); + }); + fromSet.observe({ entryTypes: new Set(["mark"]) }); + performance.mark("set-observed"); + }); + + it("Should refuse to switch an observer between the entryTypes and type forms", function () { + var byList = observing(function () {}); + byList.observe({ entryTypes: ["mark"] }); + expectThrowsNamed(function () { byList.observe({ type: "measure" }); }, "InvalidModificationError"); + + var bySingle = observing(function () {}); + bySingle.observe({ type: "mark" }); + expectThrowsNamed(function () { bySingle.observe({ entryTypes: ["measure"] }); }, "InvalidModificationError"); + }); + + it("Should deliver entries asynchronously, after mark() has returned", function (done) { + var markReturned = false; + var observer = observing(function (list, self) { + expect(markReturned).toBe(true); + expect(this).toBe(observer); + expect(self).toBe(observer); + expect(list instanceof PerformanceObserverEntryList).toBe(true); + expect(Object.prototype.toString.call(list)).toBe("[object PerformanceObserverEntryList]"); + + var entries = list.getEntries(); + expect(entries.length).toBe(1); + expect(entries[0].name).toBe("async"); + expect(entries[0].entryType).toBe("mark"); + expect(entryNames(list.getEntriesByType("mark"))).toEqual(["async"]); + expect(list.getEntriesByType("measure")).toEqual([]); + expect(entryNames(list.getEntriesByName("async"))).toEqual(["async"]); + expect(list.getEntriesByName("async", "mark").length).toBe(1); + expect(list.getEntriesByName("async", "measure")).toEqual([]); + expect(list.getEntriesByName("nothing")).toEqual([]); + done(); + }); + + observer.observe({ entryTypes: ["mark"] }); + performance.mark("async"); + markReturned = true; + }); + + it("Should observe a single entry type with the type form", function (done) { + var finished = false; + var observer = observing(function (list) { + if (finished) { + return; + } + finished = true; + var entries = list.getEntries(); + expect(entries.length).toBe(1); + expect(entries[0].name).toBe("only-measure"); + expect(entries[0].entryType).toBe("measure"); + done(); + }); + + observer.observe({ type: "measure" }); + performance.mark("ignored"); + performance.measure("only-measure", { start: 0, end: 1 }); + }); + + it("Should replay already buffered entries when buffered is true", function (done) { + performance.mark("before-observe", { startTime: 1 }); + var observer = observing(function (list) { + expect(entryNames(list.getEntries())).toContain("before-observe"); + done(); + }); + + observer.observe({ type: "mark", buffered: true }); + }); + + it("Should ignore unsupported entry types and keep the supported ones", function (done) { + var observer = observing(function (list) { + expect(entryNames(list.getEntries())).toEqual(["filtered"]); + done(); + }); + + observer.observe({ entryTypes: ["mark", "resource", "navigation"] }); + performance.mark("filtered"); + }); + + it("Should stay silent when every requested entry type is unsupported", function (done) { + var calls = 0; + var observer = observing(function () { calls++; }); + + observer.observe({ entryTypes: ["resource", "navigation"] }); + performance.mark("unobserved"); + performance.measure("unobserved", { start: 0, end: 1 }); + + setTimeout(function () { + expect(calls).toBe(0); + done(); + }, 500); + }); + + it("Should drain pending entries synchronously with takeRecords", function (done) { + var calls = 0; + var observer = observing(function () { calls++; }); + observer.observe({ entryTypes: ["mark"] }); + performance.mark("taken"); + + var records = observer.takeRecords(); + expect(records.length).toBe(1); + expect(records[0].name).toBe("taken"); + expect(observer.takeRecords()).toEqual([]); + + setTimeout(function () { + expect(calls).toBe(0); + done(); + }, 500); + }); + + it("Should stop delivering after disconnect", function (done) { + var calls = 0; + var observer = observing(function () { calls++; }); + observer.observe({ entryTypes: ["mark", "measure"] }); + observer.disconnect(); + + performance.mark("after-disconnect"); + performance.measure("after-disconnect", { start: 0, end: 1 }); + + setTimeout(function () { + expect(calls).toBe(0); + expect(observer.takeRecords()).toEqual([]); + done(); + }, 500); + }); + + it("Should run every observer in registration order even when one throws", function (done) { + var order = []; + var restoreErrorHandling = suppressGlobalErrors(); + + observing(function () { order.push("first"); }).observe({ entryTypes: ["mark"] }); + observing(function () { + order.push("second"); + throw new Error("observer callback failure"); + }).observe({ entryTypes: ["mark"] }); + observing(function () { order.push("third"); }).observe({ entryTypes: ["mark"] }); + + performance.mark("fan-out"); + + setTimeout(function () { + expect(order).toEqual(["first", "second", "third"]); + restoreErrorHandling(); + done(); + }, 500); + }); +}); + +describe("Performance in workers", function () { + // Worker paths are resolved against the requiring module's directory. + var EVAL_WORKER = "../Workers/EvalWorker.js"; + var originalTimeout; + + beforeEach(function () { + originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 8000; + clearTimeline(); + }); + + afterEach(function () { + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; + }); + + it("Should expose the same performance surface", function (done) { + var worker = new Worker(EVAL_WORKER); + + worker.postMessage({ + eval: "postMessage({" + + "performance: typeof performance," + + "now: typeof performance.now()," + + "timeOrigin: typeof performance.timeOrigin," + + "isPerformance: performance instanceof Performance," + + "isEventTarget: performance instanceof EventTarget," + + "tag: Object.prototype.toString.call(performance)," + + "mark: typeof performance.mark," + + "measure: typeof performance.measure," + + "getEntries: typeof performance.getEntries," + + "clearMarks: typeof performance.clearMarks," + + "observer: typeof PerformanceObserver," + + "entry: typeof PerformanceEntry," + + "markCtor: typeof PerformanceMark," + + "measureCtor: typeof PerformanceMeasure," + + "entryList: typeof PerformanceObserverEntryList" + + "});" + }); + + worker.onmessage = function (msg) { + expect(msg.data.performance).toBe("object"); + expect(msg.data.now).toBe("number"); + expect(msg.data.timeOrigin).toBe("number"); + expect(msg.data.isPerformance).toBe(true); + expect(msg.data.isEventTarget).toBe(true); + expect(msg.data.tag).toBe("[object Performance]"); + expect(msg.data.mark).toBe("function"); + expect(msg.data.measure).toBe("function"); + expect(msg.data.getEntries).toBe("function"); + expect(msg.data.clearMarks).toBe("function"); + expect(msg.data.observer).toBe("function"); + expect(msg.data.entry).toBe("function"); + expect(msg.data.markCtor).toBe("function"); + expect(msg.data.measureCtor).toBe("function"); + expect(msg.data.entryList).toBe("function"); + worker.terminate(); + done(); + }; + }); + + it("Should capture its own time origin when the worker thread starts", function (done) { + var mainTimeOrigin = performance.timeOrigin; + var mainNowBeforeWorker = performance.now(); + var worker = new Worker(EVAL_WORKER); + + worker.postMessage({ + // Min-of-N for the same reason as the main-isolate spec: a stall + // between the clock reads must not read as an anchoring error. + eval: "var minOffset = Infinity;" + + "for (var i = 0; i < 10; i++) {" + + " var offset = Math.abs(Date.now() - (performance.timeOrigin + performance.now()));" + + " if (offset < minOffset) { minOffset = offset; }" + + "}" + + "postMessage({ timeOrigin: performance.timeOrigin, now: performance.now(), minOffset: minOffset });" + }); + + worker.onmessage = function (msg) { + var mainDate = Date.now(); + expect(msg.data.timeOrigin).not.toBeLessThan(mainTimeOrigin); + // The worker clock only starts running with its thread, so the time it + // has accumulated stays below what the main isolate had already logged + // before the worker existed. + expect(msg.data.now).toBeLessThan(mainNowBeforeWorker); + expect(msg.data.minOffset).toBeLessThan(250); + expect(Math.abs(mainDate - (msg.data.timeOrigin + msg.data.now))).toBeLessThan(500); + worker.terminate(); + done(); + }; + }); + + it("Should keep a timeline buffer independent from the main isolate", function (done) { + performance.mark("main-only"); + expect(entryNames(performance.getEntries())).toEqual(["main-only"]); + + var worker = new Worker(EVAL_WORKER); + + worker.postMessage({ + eval: "var initial = performance.getEntries().length;" + + "performance.mark('worker-only');" + + "postMessage({ initial: initial, names: performance.getEntries().map(function (entry) { return entry.name; }) });" + }); + + worker.onmessage = function (msg) { + expect(msg.data.initial).toBe(0); + expect(msg.data.names).toEqual(["worker-only"]); + expect(entryNames(performance.getEntries())).toEqual(["main-only"]); + worker.terminate(); + done(); + }; + }); +}); diff --git a/StructuredClone/index.js b/StructuredClone/index.js new file mode 100644 index 0000000..e6dca7a --- /dev/null +++ b/StructuredClone/index.js @@ -0,0 +1,741 @@ +// Suite for the WHATWG structuredClone() global. +// +// The suite gates itself on the API being present, so it can sit in +// runAllTests() on every runtime and report a visible pending spec where +// structuredClone does not exist yet, rather than being wired in per-runtime. +// +// A runtime that DOES implement structuredClone must keep an unguarded canary +// in its own suite asserting the global is there (on iOS: +// TestRunner/app/tests/RuntimeImplementedAPIs.js). Without one, this gate would +// quietly turn a regression that removed the API into a skipped suite. +// +// Clone failures are asserted by `.name === "DataCloneError"` rather than by +// `instanceof DOMException`: runtimes without a DOMException throw a plain +// Error carrying that name. + +if (typeof global.structuredClone !== "function") { + describe("structuredClone", function () { + it("is skipped: this runtime does not implement structuredClone", function () { + pending(); + }); + }); + return; +} + +// The V8-based iOS runtime (@nativescript/ios); the legacy JSC runtime exposes TNSRuntime +var isV8iOS = !!global.NSObject && !global.TNSRuntime; + +// Not every runtime exposes shared memory; the sharing specs feature-detect. +var hasSharedArrayBuffer = typeof SharedArrayBuffer === "function"; + +function expectThrowsNamed(name, fn) { + var thrown = null; + try { + fn(); + } catch (e) { + thrown = e; + } + expect(thrown).not.toBeNull(); + expect(thrown && thrown.name).toBe(name); +} + +function expectThrowsTypeError(fn) { + var thrown = null; + try { + fn(); + } catch (e) { + thrown = e; + } + expect(thrown).not.toBeNull(); + expect(thrown instanceof TypeError).toBe(true); +} + +describe(module.id, function () { + it("is a function on the global", function () { + expect(typeof structuredClone).toBe("function"); + expect(structuredClone.length).toBe(1); + expect(structuredClone.name).toBe("structuredClone"); + }); + + describe("primitives", function () { + it("round-trips numbers, including the special values", function () { + expect(structuredClone(0)).toBe(0); + expect(structuredClone(42)).toBe(42); + expect(structuredClone(-1.5)).toBe(-1.5); + expect(Object.is(structuredClone(-0), -0)).toBe(true); + expect(isNaN(structuredClone(NaN))).toBe(true); + expect(structuredClone(Infinity)).toBe(Infinity); + expect(structuredClone(-Infinity)).toBe(-Infinity); + }); + + it("round-trips strings, booleans, null and undefined", function () { + expect(structuredClone("")).toBe(""); + expect(structuredClone("héllo \u{1F600}")).toBe("héllo \u{1F600}"); + expect(structuredClone(true)).toBe(true); + expect(structuredClone(false)).toBe(false); + expect(structuredClone(null)).toBeNull(); + expect(structuredClone(undefined)).toBeUndefined(); + }); + + it("round-trips BigInt", function () { + var big = BigInt("9007199254740993"); + var cloned = structuredClone(big); + expect(typeof cloned).toBe("bigint"); + expect(cloned === big).toBe(true); + expect(structuredClone(BigInt(-7)) === BigInt(-7)).toBe(true); + }); + }); + + describe("plain objects and arrays", function () { + it("clones a deeply nested structure by value", function () { + var source = { a: 1, b: { c: [1, 2, { d: "deep" }], e: null }, f: [[["nested"]]] }; + var cloned = structuredClone(source); + + expect(cloned).not.toBe(source); + expect(cloned.b).not.toBe(source.b); + expect(cloned.b.c[2]).not.toBe(source.b.c[2]); + expect(cloned.b.c[2].d).toBe("deep"); + expect(cloned.f[0][0][0]).toBe("nested"); + }); + + it("preserves property order", function () { + var source = { z: 1, a: 2, m: 3, "0": 4 }; + expect(Object.keys(structuredClone(source)).join(",")).toBe(Object.keys(source).join(",")); + }); + + it("clones arrays, including holes and extra properties", function () { + var source = [1, , 3]; + source.extra = "x"; + var cloned = structuredClone(source); + + expect(Array.isArray(cloned)).toBe(true); + expect(cloned.length).toBe(3); + expect(cloned.hasOwnProperty(1)).toBe(false); + expect(cloned.extra).toBe("x"); + }); + + it("is deep: mutating either side does not affect the other", function () { + var source = { list: [1, 2, 3], nested: { n: 1 } }; + var cloned = structuredClone(source); + + cloned.list.push(4); + cloned.nested.n = 99; + expect(source.list.length).toBe(3); + expect(source.nested.n).toBe(1); + + source.nested.n = 7; + expect(cloned.nested.n).toBe(99); + }); + + it("drops the prototype of a class instance", function () { + function Thing(v) { + this.v = v; + } + Thing.prototype.method = function () { }; + + var cloned = structuredClone(new Thing(5)); + expect(cloned.v).toBe(5); + expect(cloned instanceof Thing).toBe(false); + expect(Object.getPrototypeOf(cloned)).toBe(Object.prototype); + }); + + it("invokes getters and stores their value as a data property", function () { + var calls = 0; + var source = { + get computed() { + calls++; + return { inner: 1 }; + } + }; + + var cloned = structuredClone(source); + expect(calls).toBe(1); + expect(cloned.computed.inner).toBe(1); + expect(Object.getOwnPropertyDescriptor(cloned, "computed").get).toBeUndefined(); + }); + }); + + describe("built-in object types", function () { + it("clones Date", function () { + var source = new Date(1234567890123); + var cloned = structuredClone(source); + + expect(cloned instanceof Date).toBe(true); + expect(cloned).not.toBe(source); + expect(cloned.getTime()).toBe(source.getTime()); + }); + + it("clones RegExp with its flags and source", function () { + var source = /a(b)c/gimy; + var cloned = structuredClone(source); + + expect(cloned instanceof RegExp).toBe(true); + expect(cloned.source).toBe("a(b)c"); + expect(cloned.flags).toBe(source.flags); + expect(cloned.test("abc")).toBe(true); + }); + + it("clones Map, keeping entry order and cloning keys and values", function () { + var key = { k: 1 }; + var source = new Map(); + source.set("first", 1); + source.set(key, { v: 2 }); + source.set(3, "third"); + + var cloned = structuredClone(source); + expect(cloned instanceof Map).toBe(true); + expect(cloned.size).toBe(3); + expect(cloned.get("first")).toBe(1); + expect(cloned.get(3)).toBe("third"); + expect(cloned.get(key)).toBeUndefined(); + + var keys = []; + cloned.forEach(function (value, k) { keys.push(k); }); + expect(keys[0]).toBe("first"); + expect(typeof keys[1]).toBe("object"); + expect(keys[1].k).toBe(1); + expect(keys[2]).toBe(3); + }); + + it("clones Set, keeping insertion order", function () { + var source = new Set(["a", 2, "a"]); + var cloned = structuredClone(source); + + expect(cloned instanceof Set).toBe(true); + expect(cloned.size).toBe(2); + expect(cloned.has("a")).toBe(true); + expect(cloned.has(2)).toBe(true); + + var values = []; + cloned.forEach(function (v) { values.push(v); }); + expect(values.join(",")).toBe("a,2"); + }); + + it("clones Boolean, String and Number wrapper objects", function () { + var boolean = structuredClone(new Boolean(true)); + expect(typeof boolean).toBe("object"); + expect(boolean instanceof Boolean).toBe(true); + expect(boolean.valueOf()).toBe(true); + + var string = structuredClone(new String("wrapped")); + expect(string instanceof String).toBe(true); + expect(string.valueOf()).toBe("wrapped"); + + var number = structuredClone(new Number(7.5)); + expect(number instanceof Number).toBe(true); + expect(number.valueOf()).toBe(7.5); + }); + + it("clones Error, preserving name and message", function () { + var source = new TypeError("boom"); + var cloned = structuredClone(source); + + expect(cloned instanceof Error).toBe(true); + expect(cloned).not.toBe(source); + expect(cloned.name).toBe("TypeError"); + expect(cloned.message).toBe("boom"); + + var plain = structuredClone(new Error("plain")); + expect(plain.name).toBe("Error"); + expect(plain.message).toBe("plain"); + }); + }); + + describe("binary data", function () { + it("copies an ArrayBuffer without detaching the source", function () { + var source = new ArrayBuffer(4); + new Uint8Array(source).set([1, 2, 3, 4]); + + var cloned = structuredClone(source); + expect(cloned instanceof ArrayBuffer).toBe(true); + expect(cloned).not.toBe(source); + expect(cloned.byteLength).toBe(4); + expect(source.byteLength).toBe(4); + + var clonedBytes = new Uint8Array(cloned); + expect(clonedBytes[0]).toBe(1); + expect(clonedBytes[3]).toBe(4); + + clonedBytes[0] = 42; + expect(new Uint8Array(source)[0]).toBe(1); + }); + + it("clones typed arrays of every element type", function () { + var constructors = [Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, + Int32Array, Uint32Array, Float32Array, Float64Array]; + + for (var i = 0; i < constructors.length; i++) { + var Ctor = constructors[i]; + var source = new Ctor([1, 2, 3]); + var cloned = structuredClone(source); + + expect(cloned instanceof Ctor).toBe(true); + expect(cloned.length).toBe(3); + expect(cloned[0]).toBe(1); + expect(cloned[2]).toBe(3); + } + }); + + it("preserves a typed array's byteOffset and length", function () { + var buffer = new ArrayBuffer(16); + new Uint8Array(buffer).set([0, 0, 0, 0, 9, 8, 7, 6, 0, 0, 0, 0, 0, 0, 0, 0]); + var source = new Uint8Array(buffer, 4, 4); + + var cloned = structuredClone(source); + expect(cloned.byteOffset).toBe(4); + expect(cloned.length).toBe(4); + expect(cloned.buffer.byteLength).toBe(16); + expect(cloned[0]).toBe(9); + expect(cloned[3]).toBe(6); + }); + + it("clones a DataView over its slice of the buffer", function () { + var buffer = new ArrayBuffer(12); + var source = new DataView(buffer, 4, 8); + source.setFloat64(0, 1.5); + + var cloned = structuredClone(source); + expect(cloned instanceof DataView).toBe(true); + expect(cloned.byteOffset).toBe(4); + expect(cloned.byteLength).toBe(8); + expect(cloned.buffer.byteLength).toBe(12); + expect(cloned.getFloat64(0)).toBe(1.5); + }); + + it("keeps views over one buffer sharing one cloned buffer", function () { + var buffer = new ArrayBuffer(8); + var cloned = structuredClone({ a: new Uint8Array(buffer), b: new Uint8Array(buffer) }); + + expect(cloned.a.buffer).toBe(cloned.b.buffer); + cloned.a[0] = 5; + expect(cloned.b[0]).toBe(5); + }); + }); + + describe("SharedArrayBuffer", function () { + if (!hasSharedArrayBuffer) { + it("is not exposed by this runtime", function () { + expect(typeof SharedArrayBuffer).toBe("undefined"); + }); + return; + } + + it("shares its memory with the clone instead of copying it", function () { + var shared = new SharedArrayBuffer(8); + var source = new Uint8Array(shared); + source[0] = 1; + + var cloned = structuredClone(shared); + expect(cloned instanceof SharedArrayBuffer).toBe(true); + expect(cloned).not.toBe(shared); + expect(cloned.byteLength).toBe(8); + + // Written through the clone, read through the original. + new Uint8Array(cloned)[3] = 42; + expect(source[3]).toBe(42); + + // And the other way around. + source[7] = 9; + expect(new Uint8Array(cloned)[7]).toBe(9); + }); + + it("keeps one shared buffer shared across two references", function () { + var shared = new SharedArrayBuffer(4); + var cloned = structuredClone({ a: shared, b: shared }); + + expect(cloned.a).toBe(cloned.b); + new Uint8Array(cloned.a)[0] = 7; + expect(new Uint8Array(shared)[0]).toBe(7); + }); + + it("is not transferable", function () { + var shared = new SharedArrayBuffer(4); + expectThrowsNamed("DataCloneError", function () { + structuredClone(shared, { transfer: [shared] }); + }); + }); + }); + + describe("graph shape", function () { + it("preserves identity of an object referenced twice", function () { + var shared = { s: 1 }; + var cloned = structuredClone({ x: shared, y: shared }); + + expect(cloned.x).toBe(cloned.y); + expect(cloned.x).not.toBe(shared); + + cloned.x.s = 2; + expect(cloned.y.s).toBe(2); + expect(shared.s).toBe(1); + }); + + it("round-trips a self-referencing object", function () { + var source = { name: "root" }; + source.self = source; + + var cloned = structuredClone(source); + expect(cloned.name).toBe("root"); + expect(cloned.self).toBe(cloned); + expect(cloned.self).not.toBe(source); + }); + + it("round-trips a longer cycle through arrays and Maps", function () { + var a = { name: "a" }; + var b = { name: "b", a: a }; + a.b = b; + a.list = [a, b]; + + var map = new Map(); + map.set("a", a); + a.map = map; + + var cloned = structuredClone(a); + expect(cloned.b.a).toBe(cloned); + expect(cloned.list[0]).toBe(cloned); + expect(cloned.list[1]).toBe(cloned.b); + expect(cloned.map.get("a")).toBe(cloned); + }); + }); + + describe("uncloneable values", function () { + it("throws DataCloneError for a function", function () { + expectThrowsNamed("DataCloneError", function () { + structuredClone(function () { }); + }); + expectThrowsNamed("DataCloneError", function () { + structuredClone({ fn: function () { } }); + }); + }); + + it("throws DataCloneError for a symbol", function () { + expectThrowsNamed("DataCloneError", function () { + structuredClone(Symbol("nope")); + }); + expectThrowsNamed("DataCloneError", function () { + structuredClone({ sym: Symbol("nope") }); + }); + }); + + it("throws DataCloneError for WeakMap and WeakSet", function () { + expectThrowsNamed("DataCloneError", function () { + structuredClone(new WeakMap()); + }); + expectThrowsNamed("DataCloneError", function () { + structuredClone(new WeakSet()); + }); + }); + + it("throws DataCloneError for a WeakRef", function () { + expectThrowsNamed("DataCloneError", function () { + structuredClone(new WeakRef({})); + }); + }); + + it("throws DataCloneError for a Promise", function () { + expectThrowsNamed("DataCloneError", function () { + structuredClone(Promise.resolve(1)); + }); + }); + + it("leaves nothing broken after a failed clone", function () { + var source = { ok: 1, bad: function () { } }; + expectThrowsNamed("DataCloneError", function () { + structuredClone(source); + }); + expect(structuredClone({ ok: source.ok }).ok).toBe(1); + }); + + if (isV8iOS) { + it("throws DataCloneError for a native object", function () { + expectThrowsNamed("DataCloneError", function () { + structuredClone(NSObject.alloc().init()); + }); + }); + } + }); + + describe("transfer", function () { + it("detaches the source buffer and hands over its memory", function () { + var source = new ArrayBuffer(4); + new Uint8Array(source).set([1, 2, 3, 4]); + + var cloned = structuredClone(source, { transfer: [source] }); + expect(cloned instanceof ArrayBuffer).toBe(true); + expect(cloned.byteLength).toBe(4); + expect(new Uint8Array(cloned)[2]).toBe(3); + expect(source.byteLength).toBe(0); + }); + + it("transfers a buffer reached through a typed array in the value", function () { + var buffer = new ArrayBuffer(8); + var view = new Uint8Array(buffer); + view[0] = 7; + view[7] = 9; + + var cloned = structuredClone({ view: view }, { transfer: [buffer] }); + expect(cloned.view.length).toBe(8); + expect(cloned.view[0]).toBe(7); + expect(cloned.view[7]).toBe(9); + expect(buffer.byteLength).toBe(0); + expect(view.length).toBe(0); + }); + + it("transfers several buffers at once", function () { + var first = new ArrayBuffer(2); + var second = new ArrayBuffer(3); + + var cloned = structuredClone({ first: first, second: second }, { transfer: [first, second] }); + expect(cloned.first.byteLength).toBe(2); + expect(cloned.second.byteLength).toBe(3); + expect(first.byteLength).toBe(0); + expect(second.byteLength).toBe(0); + }); + + it("transfers a buffer that is not part of the cloned value", function () { + var unrelated = new ArrayBuffer(4); + var cloned = structuredClone({ n: 1 }, { transfer: [unrelated] }); + + expect(cloned.n).toBe(1); + expect(unrelated.byteLength).toBe(0); + }); + + it("accepts any iterable as the transfer list", function () { + var buffer = new ArrayBuffer(4); + var cloned = structuredClone(buffer, { transfer: new Set([buffer]) }); + + expect(cloned.byteLength).toBe(4); + expect(buffer.byteLength).toBe(0); + }); + + it("reads the transfer iterator's next method only once", function () { + var buffer = new ArrayBuffer(4); + var nextReads = 0; + var exhausted = false; + + function next() { + if (exhausted) { + return { done: true, value: undefined }; + } + exhausted = true; + return { done: false, value: buffer }; + } + + var iterator = {}; + Object.defineProperty(iterator, "next", { + get: function () { + nextReads++; + if (nextReads > 1) { + throw new Error("next must be captured once, not re-read per step"); + } + return next; + } + }); + + var iterable = {}; + iterable[Symbol.iterator] = function () { + return iterator; + }; + + var cloned = structuredClone(buffer, { transfer: iterable }); + expect(nextReads).toBe(1); + expect(cloned.byteLength).toBe(4); + expect(buffer.byteLength).toBe(0); + }); + + it("accepts an absent, undefined or empty transfer list", function () { + var buffer = new ArrayBuffer(4); + expect(structuredClone(buffer, {}).byteLength).toBe(4); + expect(structuredClone(buffer, { transfer: undefined }).byteLength).toBe(4); + expect(structuredClone(buffer, { transfer: [] }).byteLength).toBe(4); + expect(buffer.byteLength).toBe(4); + }); + + it("throws DataCloneError when the same buffer is listed twice", function () { + var buffer = new ArrayBuffer(4); + expectThrowsNamed("DataCloneError", function () { + structuredClone(buffer, { transfer: [buffer, buffer] }); + }); + }); + + it("throws DataCloneError for a non-transferable entry", function () { + expectThrowsNamed("DataCloneError", function () { + structuredClone({}, { transfer: [{}] }); + }); + expectThrowsNamed("DataCloneError", function () { + structuredClone({}, { transfer: [5] }); + }); + expectThrowsNamed("DataCloneError", function () { + structuredClone({}, { transfer: [new Uint8Array(4)] }); + }); + }); + + it("throws DataCloneError for an already detached buffer", function () { + var buffer = new ArrayBuffer(4); + structuredClone(buffer, { transfer: [buffer] }); + + expectThrowsNamed("DataCloneError", function () { + structuredClone(buffer, { transfer: [buffer] }); + }); + expectThrowsNamed("DataCloneError", function () { + structuredClone(buffer); + }); + }); + + it("throws TypeError for a non-iterable transfer list", function () { + expectThrowsTypeError(function () { + structuredClone({}, { transfer: 5 }); + }); + expectThrowsTypeError(function () { + structuredClone({}, { transfer: null }); + }); + expectThrowsTypeError(function () { + structuredClone({}, { transfer: {} }); + }); + expectThrowsTypeError(function () { + structuredClone({}, { transfer: "abc" }); + }); + }); + }); + + describe("arguments", function () { + it("throws TypeError when called without a value", function () { + expectThrowsTypeError(function () { + structuredClone(); + }); + }); + + it("throws TypeError when options is not an object", function () { + expectThrowsTypeError(function () { + structuredClone({}, 5); + }); + expectThrowsTypeError(function () { + structuredClone({}, "transfer"); + }); + }); + + it("accepts undefined and null options", function () { + expect(structuredClone(1, undefined)).toBe(1); + expect(structuredClone(1, null)).toBe(1); + }); + }); + + describe("workers", function () { + var originalTimeout; + + beforeEach(function () { + originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 8000; // For slower android emulators + }); + + afterEach(function () { + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; + }); + + it("is available inside a worker", function (done) { + var worker = new Worker("../Workers/EvalWorker.js"); + worker.onmessage = function (msg) { + expect(msg.data.t).toBe("function"); + worker.terminate(); + done(); + }; + worker.postMessage({ eval: "postMessage({ t: typeof structuredClone })" }); + }); + + // postMessage runs on the same serialization core as structuredClone, + // so the transfer rules below are the same ones asserted above. Its + // transfer list is a plain array only: there is no JS wrapper around + // postMessage to do the WebIDL iterable conversion. + it("transfers an ArrayBuffer to a worker and detaches the source", function (done) { + var worker = new Worker("../Workers/EvalWorker.js"); + var buffer = new ArrayBuffer(4); + new Uint8Array(buffer).set([5, 6, 7, 8]); + + worker.onmessage = function (msg) { + expect(msg.data.len).toBe(4); + expect(msg.data.first).toBe(5); + expect(msg.data.last).toBe(8); + worker.terminate(); + done(); + }; + + worker.postMessage({ + value: buffer, + eval: "postMessage({ len: value.byteLength, first: new Uint8Array(value)[0], last: new Uint8Array(value)[3] })" + }, [buffer]); + + expect(buffer.byteLength).toBe(0); + }); + + it("copies the buffer when it is not in the transfer list", function (done) { + var worker = new Worker("../Workers/EvalWorker.js"); + var buffer = new ArrayBuffer(4); + new Uint8Array(buffer).set([1, 2, 3, 4]); + + worker.onmessage = function (msg) { + expect(msg.data.len).toBe(4); + worker.terminate(); + done(); + }; + + worker.postMessage({ + value: buffer, + eval: "postMessage({ len: value.byteLength })" + }); + + expect(buffer.byteLength).toBe(4); + }); + + it("throws TypeError for a transfer list that is not an array", function () { + var worker = new Worker("../Workers/EvalWorker.js"); + expectThrowsTypeError(function () { + worker.postMessage({ value: 1 }, 5); + }); + expectThrowsTypeError(function () { + worker.postMessage({ value: 1 }, "buffer"); + }); + worker.terminate(); + }); + + it("throws DataCloneError for bad transfer list entries", function () { + var worker = new Worker("../Workers/EvalWorker.js"); + + var duplicated = new ArrayBuffer(4); + expectThrowsNamed("DataCloneError", function () { + worker.postMessage({ value: duplicated }, [duplicated, duplicated]); + }); + expect(duplicated.byteLength).toBe(4); + + expectThrowsNamed("DataCloneError", function () { + worker.postMessage({ value: 1 }, [{}]); + }); + + var detached = new ArrayBuffer(4); + structuredClone(detached, { transfer: [detached] }); + expectThrowsNamed("DataCloneError", function () { + worker.postMessage({ value: 1 }, [detached]); + }); + + worker.terminate(); + }); + + if (isV8iOS) { + // Deliberate asymmetry with structuredClone, which rejects host + // objects: posting a native object has always delivered an empty + // object rather than throwing, and Workers/index.js pins that it + // does not throw. Only the runtime's HostObjectPolicy encodes it. + it("delivers a posted native object as an empty object", function (done) { + var worker = new Worker("../Workers/EvalWorker.js"); + worker.onmessage = function (msg) { + expect(msg.data.type).toBe("object"); + expect(msg.data.keys).toBe(0); + worker.terminate(); + done(); + }; + worker.postMessage({ + value: NSObject.alloc().init(), + eval: "postMessage({ type: typeof value, keys: Object.keys(value).length })" + }); + }); + } + }); +}); diff --git a/index.js b/index.js index 0724d36..f7cbe1f 100644 --- a/index.js +++ b/index.js @@ -18,10 +18,20 @@ exports.runWorkerTests = function() { require("./Workers"); } +exports.runPerformanceTests = function() { + require("./Performance"); +} + +exports.runStructuredCloneTests = function() { + require("./StructuredClone"); +} + exports.runAllTests = function() { exports.runImportTests(); exports.runRequireTests(); exports.runWeakRefTests(); exports.runRuntimeTests(); exports.runWorkerTests(); + exports.runPerformanceTests(); + exports.runStructuredCloneTests(); }