Last active
November 11, 2025 18:40
-
-
Save zbraniecki/4a9c6725b2b6c22aaf05d60e67d3c45f to your computer and use it in GitHub Desktop.
Temporal Benchmarks between V8, SM & Boa
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Temporal benchmark starting... | |
| TimeZones: UTC, America/Los_Angeles, Europe/Warsaw, Asia/Shanghai | |
| Calendars: iso8601, gregory, hebrew, chinese, japanese, buddhist, persian | |
| All results are higher-is-better (ops/sec). | |
| Offset lookup via ZDT (proxy for getOffset) | ops/sec mean=1025339 median=1024000 p95=1028016 (n=6) | |
| ZonedDateTime.from Instant+TZ | ops/sec mean=887657 median=885622 p95=891646 (n=6) | |
| ZonedDateTime.add across DST | ops/sec mean=470945 median=471482 p95=474899 (n=6) | |
| PlainDate dateAdd (months/years/leap) | ops/sec mean=1499564 median=1502270 p95=1506575 (n=6) | |
| PlainDateTime since/until (varied units) | ops/sec mean=628153 median=627139 p95=630154 (n=6) | |
| ZonedDateTime.round (minute/hour/day) | ops/sec mean=856836 median=859498 p95=868026 (n=6) | |
| Calendar daysInMonth/daysInYear (via PlainDate) | ops/sec mean=1182064 median=1180839 p95=1191564 (n=6) | |
| Instant parse → toString → parse | ops/sec mean=555016 median=554218 p95=557753 (n=6) | |
| ZDT disambiguation (earlier/later) | ops/sec mean=161503 median=161315 p95=162428 (n=6) | |
| Done. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Temporal benchmark starting... | |
| TimeZones: UTC, America/Los_Angeles, Europe/Warsaw, Asia/Shanghai | |
| Calendars: iso8601, gregory, hebrew, chinese, japanese, buddhist, persian | |
| All results are higher-is-better (ops/sec). | |
| Offset lookup via ZDT (proxy for getOffset) | ops/sec mean=1962124 median=1951565 p95=1985939 (n=6) | |
| ZonedDateTime.from Instant+TZ | ops/sec mean=1906861 median=1904425 p95=1944688 (n=6) | |
| ZonedDateTime.add across DST | ops/sec mean=761141 median=760617 p95=773286 (n=6) | |
| PlainDate dateAdd (months/years/leap) | ops/sec mean=1638710 median=1635960 p95=1649742 (n=6) | |
| PlainDateTime since/until (varied units) | ops/sec mean=621964 median=627305 p95=630760 (n=6) | |
| ZonedDateTime.round (minute/hour/day) | ops/sec mean=2024578 median=1987462 p95=2073019 (n=6) | |
| Calendar daysInMonth/daysInYear (via PlainDate) | ops/sec mean=2337257 median=2360325 p95=2413849 (n=6) | |
| Instant parse → toString → parse | ops/sec mean=922271 median=918877 p95=928272 (n=6) | |
| ZDT disambiguation (earlier/later) | ops/sec mean=542328 median=541847 p95=549108 (n=6) | |
| Done. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Temporal benchmark starting... | |
| TimeZones: UTC, America/Los_Angeles, Europe/Warsaw, Asia/Shanghai | |
| Calendars: iso8601, gregory, hebrew, chinese, japanese, buddhist, persian | |
| All results are higher-is-better (ops/sec). | |
| Offset lookup via ZDT (proxy for getOffset) | ops/sec mean=901034 median=900849 p95=910222 (n=6) | |
| ZonedDateTime.from Instant+TZ | ops/sec mean=4043495 median=4042925 p95=4153690 (n=6) | |
| ZonedDateTime.add across DST | ops/sec mean=2214390 median=2214155 p95=2244384 (n=6) | |
| PlainDate dateAdd (months/years/leap) | ops/sec mean=16978086 median=16944669 p95=16944669 (n=6) | |
| PlainDateTime since/until (varied units) | ops/sec mean=2612837 median=2612760 p95=2621440 (n=6) | |
| ZonedDateTime.round (minute/hour/day) | ops/sec mean=5754449 median=5740380 p95=6199351 (n=6) | |
| Calendar daysInMonth/daysInYear (via PlainDate) | ops/sec mean=103667 median=104000 p95=104000 (n=6) | |
| Instant parse → toString → parse | ops/sec mean=3324521 median=3336422 p95=3348555 (n=6) | |
| ZDT disambiguation (earlier/later) | ops/sec mean=736383 median=736360 p95=736360 (n=6) | |
| Done. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| (() => { | |
| // ---- performance.now polyfill ---- | |
| if (typeof globalThis.performance === "undefined") { | |
| const perf = {}; | |
| if ( | |
| typeof process !== "undefined" && | |
| process.hrtime && | |
| process.hrtime.bigint | |
| ) { | |
| const t0 = process.hrtime.bigint(); | |
| perf.now = () => Number(process.hrtime.bigint() - t0) / 1e6; | |
| } else if (typeof Date !== "undefined") { | |
| const d0 = Date.now(); | |
| perf.now = () => Date.now() - d0; | |
| } else if ( | |
| typeof Temporal !== "undefined" && | |
| Temporal.Now && | |
| Temporal.Now.instant | |
| ) { | |
| const i0 = Temporal.Now.instant().epochMilliseconds; | |
| perf.now = () => Temporal.Now.instant().epochMilliseconds - i0; | |
| } else { | |
| let t = 0; | |
| perf.now = () => (t += 1); | |
| } | |
| globalThis.performance = perf; | |
| } | |
| if (typeof Temporal === "undefined") { | |
| console.error("Temporal not available."); | |
| return; | |
| } | |
| // blackholes | |
| globalThis.__bh_num ??= 0; | |
| // ---- small stats ---- | |
| const stats = (a) => { | |
| const s = Float64Array.from(a).sort(); | |
| const n = s.length; | |
| const mean = s.reduce((x, y) => x + y, 0) / (n || 1); | |
| const med = n | |
| ? n & 1 | |
| ? s[(n - 1) >> 1] | |
| : 0.5 * (s[n / 2 - 1] + s[n / 2]) | |
| : 0; | |
| const p95 = n ? s[Math.min(n - 1, Math.floor(0.95 * (n - 1)))] : 0; | |
| return { mean, median: med, p95 }; | |
| }; | |
| // ---- config ---- | |
| const RUN_TARGET_MS = 600; // longer to amortize runtime overhead | |
| const REPEATS = 6; | |
| const MAX_SCALE = 1 << 24; | |
| const BATCH = 2048; // heavy per-iteration Temporal work | |
| // ---- helpers ---- | |
| let seed = 123456789; | |
| const rand = () => (seed = (1664525 * seed + 1013904223) >>> 0) / 2 ** 32; | |
| const pad2 = (x) => String(x).padStart(2, "0"); | |
| const instantFromUTC = (y, m, d, h = 0, mi = 0, s = 0) => | |
| Temporal.Instant.from( | |
| `${String(y).padStart(4, "0")}-${pad2(m)}-${pad2(d)}T${pad2(h)}:${pad2(mi)}:${pad2(s)}Z`, | |
| ); | |
| const toPow2 = (arr) => { | |
| let n = 1; | |
| while (n < arr.length) n <<= 1; | |
| if (n === arr.length) return { data: arr, mask: n - 1 }; | |
| const out = arr.slice(); | |
| for (let i = arr.length; i < n; i++) out.push(arr[i - arr.length]); | |
| return { data: out, mask: n - 1 }; | |
| }; | |
| // ---- datasets ---- | |
| const tzIdsRaw = [ | |
| "UTC", | |
| "America/Los_Angeles", | |
| "Europe/Warsaw", | |
| "Asia/Shanghai", | |
| ].filter((id) => { | |
| try { | |
| Temporal.ZonedDateTime.from({ | |
| timeZone: id, | |
| calendar: "iso8601", | |
| year: 2020, | |
| month: 1, | |
| day: 1, | |
| hour: 0, | |
| }); | |
| return true; | |
| } catch { | |
| return false; | |
| } | |
| }); | |
| const { data: tzIds, mask: TZ_MASK } = toPow2( | |
| tzIdsRaw.length ? tzIdsRaw : ["UTC"], | |
| ); | |
| const calIdsRaw = [ | |
| "gregory", | |
| "hebrew", | |
| "islamic", | |
| "chinese", | |
| "japanese", | |
| "buddhist", | |
| "persian", | |
| ]; // non-ISO only to stress rules | |
| const usableCalIds = calIdsRaw.filter((id) => { | |
| try { | |
| Temporal.PlainDate.from({ year: 2020, month: 1, day: 1 }).withCalendar( | |
| id, | |
| ); | |
| return true; | |
| } catch { | |
| return false; | |
| } | |
| }); | |
| const { data: calendarIds, mask: CAL_MASK } = toPow2( | |
| usableCalIds.length ? usableCalIds : ["gregory"], | |
| ); | |
| const instantsRaw = (() => { | |
| const years = Array.from({ length: 21 }, (_, i) => 2010 + i); | |
| const offs = [-7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7]; | |
| const out = []; | |
| for (const y of years) { | |
| for (const [m, d] of [ | |
| [3, 14], | |
| [10, 31], | |
| ]) { | |
| for (const k of offs) { | |
| const dt = new Date(Date.UTC(y, m - 1, d)); | |
| dt.setUTCDate(dt.getUTCDate() + k); | |
| out.push( | |
| instantFromUTC( | |
| dt.getUTCFullYear(), | |
| dt.getUTCMonth() + 1, | |
| dt.getUTCDate(), | |
| ), | |
| ); | |
| } | |
| } | |
| } | |
| const base = Temporal.Instant.from("2020-03-08T06:00:00Z"); | |
| for (let i = -12; i <= 12; i++) out.push(base.add({ hours: i })); | |
| return out; | |
| })(); | |
| const { data: INSTANTS, mask: INS_MASK } = toPow2(instantsRaw); | |
| const DURATIONS = [ | |
| Temporal.Duration.from({ hours: 25 }), | |
| Temporal.Duration.from({ minutes: 1439 }), | |
| Temporal.Duration.from({ months: 1 }), | |
| Temporal.Duration.from({ years: 1, days: 3 }), | |
| ]; | |
| const { data: DURS, mask: DUR_MASK } = toPow2(DURATIONS); | |
| // precompute ZDTs for one zone for rounding | |
| const ROUND_ZONE = tzIds[0]; | |
| const ZDT_FOR_ROUND = INSTANTS.map((ins) => | |
| ins.toZonedDateTimeISO(ROUND_ZONE), | |
| ); | |
| const { data: ZDT_ROUND, mask: ZR_MASK } = toPow2(ZDT_FOR_ROUND); | |
| const ROUND_OPTS = [ | |
| { smallestUnit: "minute" }, | |
| { smallestUnit: "hour" }, | |
| { smallestUnit: "day" }, | |
| ]; | |
| const { data: ROUND_OPT_ARR, mask: RO_MASK } = toPow2(ROUND_OPTS); | |
| // random PlainDates for calendar queries | |
| const DATES_RAW = (() => { | |
| const out = []; | |
| for (let i = 0; i < 4096; i++) { | |
| const y = 1200 + Math.floor(rand() * 1400); // wide range stresses non-Gregorian rules | |
| const m = 1 + Math.floor(rand() * 12); | |
| const d = 1 + Math.floor(rand() * 28); | |
| out.push(Temporal.PlainDate.from({ year: y, month: m, day: d })); | |
| } | |
| return out; | |
| })(); | |
| const { data: DATES, mask: DATE_MASK } = toPow2(DATES_RAW); | |
| // ---- harness ---- | |
| const time = (f, iters) => { | |
| const t0 = performance.now(); | |
| f(iters); | |
| return performance.now() - t0; | |
| }; | |
| const autoscale = (fn) => { | |
| let it = 1 << 6; // each iteration performs BATCH ops | |
| for (let i = 0; i < 10; i++) { | |
| const ms = time(fn, it); | |
| if (ms > RUN_TARGET_MS / 3 || it >= MAX_SCALE) break; | |
| const scale = Math.max( | |
| 2, | |
| Math.min(64, Math.ceil(RUN_TARGET_MS / 3 / Math.max(1, ms))), | |
| ); | |
| it *= scale; | |
| } | |
| return it; | |
| }; | |
| const run = (name, builder) => { | |
| try { | |
| const fn = builder(); | |
| const it = autoscale(fn); | |
| time(fn, it); // warmup | |
| const runs = []; | |
| for (let r = 0; r < REPEATS; r++) { | |
| const ms = time(fn, it); | |
| runs.push((it * BATCH) / (ms / 1000)); // report Temporal ops/sec | |
| } | |
| const s = stats(runs); | |
| console.log( | |
| `${name.padEnd(44)}| ops/sec mean=${s.mean.toFixed(0)} median=${s.median.toFixed(0)} p95=${s.p95.toFixed(0)} (n=${REPEATS})`, | |
| ); | |
| } catch (e) { | |
| console.warn(`${name} -> skipped (${e && e.message})`); | |
| } | |
| }; | |
| console.log("Temporal benchmark starting..."); | |
| console.log(`TimeZones: ${tzIds.join(", ")}`); | |
| console.log(`Calendars (non-ISO): ${calendarIds.join(", ")}`); | |
| console.log("Higher is better. Designed to minimize JS overhead."); | |
| // 1) Offset lookup via ZDT | |
| run("Offset lookup via ZDT ", () => { | |
| const zones = tzIds, | |
| ins = INSTANTS; | |
| const zMask = TZ_MASK, | |
| iMask = INS_MASK; | |
| let i = 0, | |
| j = 0, | |
| sink = 0; | |
| return (iters) => { | |
| for (let k = 0; k < iters; k++) { | |
| let b = BATCH; | |
| while (b--) { | |
| const zdt = ins[i++ & iMask].toZonedDateTimeISO(zones[j++ & zMask]); | |
| sink ^= zdt.offsetNanoseconds; | |
| } | |
| } | |
| __bh_num ^= sink; | |
| }; | |
| }); | |
| // 2) ZDT from Instant + TZ | |
| run("ZDT from Instant + TZ ", () => { | |
| const zones = tzIds, | |
| ins = INSTANTS; | |
| const zMask = TZ_MASK, | |
| iMask = INS_MASK; | |
| let i = 0, | |
| j = 0, | |
| sink = 0; | |
| return (iters) => { | |
| for (let k = 0; k < iters; k++) { | |
| let b = BATCH; | |
| while (b--) { | |
| const zdt = ins[(i += 3) & iMask].toZonedDateTimeISO( | |
| zones[j++ & zMask], | |
| ); | |
| sink ^= zdt.epochMilliseconds & 0xffff; // Number path, avoid BigInt overhead | |
| } | |
| } | |
| __bh_num ^= sink; | |
| }; | |
| }); | |
| // 3) ZDT add across DST | |
| run("ZDT.add across DST ", () => { | |
| const zone = tzIds.includes("America/Los_Angeles") | |
| ? "America/Los_Angeles" | |
| : tzIds[0]; | |
| const ins = INSTANTS, | |
| durs = DURS; | |
| const iMask = INS_MASK, | |
| dMask = DUR_MASK; | |
| let i = 0, | |
| d = 0, | |
| sink = 0; | |
| return (iters) => { | |
| for (let k = 0; k < iters; k++) { | |
| let b = BATCH; | |
| while (b--) { | |
| const zdt = ins[i++ & iMask].toZonedDateTimeISO(zone); | |
| const res = zdt.add(durs[d++ & dMask], { overflow: "constrain" }); | |
| sink ^= res.epochMilliseconds & 0xffff; | |
| } | |
| } | |
| __bh_num ^= sink; | |
| }; | |
| }); | |
| // 4) PlainDate add/sub (months/years/leap) | |
| run("PlainDate add (months/years/leap) ", () => { | |
| const dates = []; | |
| for (let y = 1600; y <= 2400; y++) | |
| dates.push( | |
| Temporal.PlainDate.from({ | |
| year: y, | |
| month: (y % 12) + 1, | |
| day: (y % 28) + 1, | |
| }), | |
| ); | |
| const { data: DSET, mask: DMSK } = toPow2(dates); | |
| const durs = [ | |
| Temporal.Duration.from({ months: 1 }), | |
| Temporal.Duration.from({ years: 1 }), | |
| Temporal.Duration.from({ years: 4, days: 1 }), | |
| ]; | |
| const { data: D2, mask: DM2 } = toPow2(durs); | |
| let i = 0, | |
| d = 0, | |
| sink = 0; | |
| return (iters) => { | |
| for (let k = 0; k < iters; k++) { | |
| let b = BATCH; | |
| while (b--) { | |
| const r = DSET[i++ & DMSK].add(D2[d++ & DM2]); | |
| sink ^= r.year; | |
| } | |
| } | |
| __bh_num ^= sink; | |
| }; | |
| }); | |
| // 5) PlainDateTime since/until | |
| run("PlainDateTime since/until (varied units) ", () => { | |
| const base = Temporal.PlainDateTime.from("2016-02-28T23:59:59.999999999"); | |
| const ends = [ | |
| base.add({ days: 365 }), | |
| base.add({ months: 13 }), | |
| base.add({ hours: 49, minutes: 5 }), | |
| ]; | |
| const opts = [ | |
| { largestUnit: "years" }, | |
| { largestUnit: "months" }, | |
| { largestUnit: "days" }, | |
| { largestUnit: "hours", smallestUnit: "minutes" }, | |
| ]; | |
| const { data: ENDS, mask: E_MASK } = toPow2(ends); | |
| const { data: OPTS, mask: O_MASK } = toPow2(opts); | |
| let i = 0, | |
| j = 0, | |
| sink = 0; | |
| return (iters) => { | |
| for (let k = 0; k < iters; k++) { | |
| let b = BATCH; | |
| while (b--) { | |
| const a = base.add({ days: i++ & 7 }); | |
| const dur = a.until(ENDS[j++ & E_MASK], OPTS[(j + 3) & O_MASK]); | |
| sink ^= dur.days ?? 0; | |
| } | |
| } | |
| __bh_num ^= sink; | |
| }; | |
| }); | |
| // 6) ZDT.round | |
| run("ZDT.round (minute/hour/day) ", () => { | |
| const base = ZDT_ROUND; | |
| const opts = ROUND_OPT_ARR; | |
| const bMask = ZR_MASK, | |
| oMask = RO_MASK; | |
| let i = 0, | |
| j = 0, | |
| sink = 0; | |
| return (iters) => { | |
| for (let k = 0; k < iters; k++) { | |
| let b = BATCH; | |
| while (b--) { | |
| const r = base[i++ & bMask].round(opts[j++ & oMask]); | |
| sink ^= r.epochMilliseconds & 0xffff; | |
| } | |
| } | |
| __bh_num ^= sink; | |
| }; | |
| }); | |
| // 7) Calendar queries via PlainDate getters (heavier set) | |
| run("Calendar queries (daysInMonth/year/leap/dow)", () => { | |
| const dates = DATES, | |
| cals = calendarIds; | |
| const dMask = DATE_MASK, | |
| cMask = CAL_MASK; | |
| let i = 0, | |
| j = 0, | |
| sink = 0; | |
| return (iters) => { | |
| for (let k = 0; k < iters; k++) { | |
| let b = BATCH; | |
| while (b--) { | |
| const d = dates[i++ & dMask].withCalendar(cals[j++ & cMask]); | |
| // multiple getters per op to amplify calendar math | |
| sink ^= | |
| d.daysInMonth ^ d.daysInYear ^ (d.inLeapYear ? 1 : 0) ^ d.dayOfWeek; | |
| } | |
| } | |
| __bh_num ^= sink; | |
| }; | |
| }); | |
| // 8) Instant arithmetic churn (no strings) | |
| run("Instant add/sub churn ", () => { | |
| const ins = INSTANTS; | |
| const iMask = INS_MASK; | |
| const durA = Temporal.Duration.from({ | |
| minutes: 37, | |
| seconds: 59, | |
| milliseconds: 7, | |
| }); | |
| const durB = Temporal.Duration.from({ hours: 13, minutes: 5 }); | |
| let i = 0, | |
| sink = 0; | |
| return (iters) => { | |
| for (let k = 0; k < iters; k++) { | |
| let b = BATCH; | |
| while (b--) { | |
| let x = ins[i++ & iMask]; | |
| x = x.add(durA).subtract(durB).add(durA); | |
| sink ^= x.epochMilliseconds & 0xffff; | |
| } | |
| } | |
| __bh_num ^= sink; | |
| }; | |
| }); | |
| // 9) Disambiguation earlier/later | |
| run("ZDT disambiguation (earlier/later) ", () => { | |
| const zone = tzIds.includes("America/Los_Angeles") | |
| ? "America/Los_Angeles" | |
| : tzIds[0]; | |
| const local = Temporal.PlainDateTime.from("2020-11-01T01:30:00"); | |
| let sink = 0; | |
| return (iters) => { | |
| for (let k = 0; k < iters; k++) { | |
| let b = BATCH; | |
| while (b--) { | |
| const earlier = Temporal.ZonedDateTime.from({ | |
| timeZone: zone, | |
| calendar: "iso8601", | |
| year: local.year, | |
| month: local.month, | |
| day: local.day, | |
| hour: local.hour, | |
| minute: local.minute, | |
| second: local.second, | |
| millisecond: local.millisecond, | |
| microsecond: local.microsecond, | |
| nanosecond: local.nanosecond, | |
| offset: undefined, | |
| disambiguation: "earlier", | |
| }); | |
| const later = Temporal.ZonedDateTime.from({ | |
| timeZone: zone, | |
| calendar: "iso8601", | |
| year: local.year, | |
| month: local.month, | |
| day: local.day, | |
| hour: local.hour, | |
| minute: local.minute, | |
| second: local.second, | |
| millisecond: local.millisecond, | |
| microsecond: local.microsecond, | |
| nanosecond: local.nanosecond, | |
| offset: undefined, | |
| disambiguation: "later", | |
| }); | |
| sink ^= | |
| (later.epochMilliseconds - earlier.epochMilliseconds) & 0xffff; | |
| } | |
| } | |
| __bh_num ^= sink; | |
| }; | |
| }); | |
| console.log("Done."); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment