perf(time): cache the IANA UTC offset instead of re-formatting per call - #320
Open
vinay-veerappa wants to merge 1 commit into
Open
vinay-veerappa wants to merge 1 commit into
vinay-veerappa wants to merge 1 commit into
Conversation
Pine time functions run ONCE PER BAR, and a script with per-bar day-key
logic makes several calls per bar. `timestamp(TZ, ...)` and
`year()/month()/dayofmonth()/hour()/dayofweek()` with an IANA timezone
each resolved that timezone through `Intl.DateTimeFormat` on EVERY call.
Profiled on a real 10k-bar chart execute (HTF_EMA-style script, ~15 IANA
time calls per bar at global scope, Node ICU):
_timestampFromIANA 58.6% self time
getDatePartsInTimezone 20.5% self time
---------------
79% of a 33s execute
Two costs are stacked there, and only fixing one leaves most of it:
new Intl.DateTimeFormat(...) ~41us <- construction
formatter.formatToParts(date) ~3.9us <- the formatting itself
parts read off a cached offset ~0.12us <- 32x cheaper than either
Memoizing the formatter instance (they are pure functions of (locale,
options) per ECMA-402) removes the 41us and takes the execute 33s -> 26s,
but timezone math is still 40% of the profile because `formatToParts`
remains per call. Caching the resolved OFFSET removes that too: 26s ->
15s, and timezone functions drop to 2% of the profile.
`src/namespaces/tzOffset.ts` is the shared primitive both paths reduce to.
## How the cache stays exact
An IANA offset is piecewise constant in UTC and the pieces are months
long. The cache stores the offset at UTC-midnight boundaries:
* a UTC day whose two boundaries AGREE carries that offset throughout, so
calendar parts are read arithmetically;
* a UTC day whose boundaries DISAGREE contains a transition, and every
instant in it falls through to the original `formatToParts` path.
The cache therefore never approximates across a transition. Adjacent days
share a boundary probe, so a forward-marching chart costs one
`formatToParts` per calendar day regardless of bar interval - strictly
cheaper than uncached even on a daily chart (one probe per bar versus one
per call).
The one assumption: no zone transitions twice within a single UTC day and
returns to the same offset. No entry in the IANA database does this.
An hour-bucketed cache would have been simpler and is WRONG:
`Australia/Lord_Howe` shifts 30 minutes at an instant that is not on a UTC
hour boundary. It is in the test matrix for that reason.
## Behaviour
`_timestampFromIANA` keeps its single-pass semantics exactly - a
wall-clock time inside a DST gap or fold is genuinely ambiguous and Pine
resolves it this way - so only the offset lookup changed.
`getDatePartsInTimezone` now derives `dayOfWeek` arithmetically instead of
parsing a localized weekday string; same result, no locale dependency.
## Tests
`tests/namespaces/timezone-offset.test.ts` uses `Intl` itself as the
oracle and sweeps EVERY hour of eight transition days across six zones
(America/New_York, Europe/London, Australia/Sydney, Asia/Kolkata,
Asia/Kathmandu, Australia/Lord_Howe), plus minute resolution across the US
spring-forward instant, plus fixed-offset spellings and the unknown-zone
fallback. One case asserts the cached offset equals the uncached offset on
a transition day AND that the day genuinely straddles a change, so it
cannot pass vacuously.
`tests/namespaces/timezone-perf.test.ts` is a regression guard: 35k bars x
5 IANA calls per bar, bounded at 2000ms. It measures ~2890ms unfixed and
~440ms fixed.
Negative control: replacing the boundary comparison with an unconditional
day-start offset fails 3 of the 6 correctness cases.
Suite: `main` is 139 failed / 1744 passed; this branch is 139 failed /
1752 passed - the same pre-existing failures, plus the 8 tests added here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
All contributors have signed the CLA ✍️ ✅ |
Author
|
I have read the CLA Document and I hereby sign the CLA |
alaa-eddine
added a commit
to LuxAlgo/cla-signatures
that referenced
this pull request
Sep 20, 2026
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
Pine time functions run once per bar, and a script with per-bar day-key logic makes several calls per bar.
timestamp(TZ, ...)andyear()/month()/dayofmonth()/hour()/dayofweek()with an IANA timezone each resolve that timezone throughIntl.DateTimeFormaton every call.Profiled on a real 10k-bar chart execute (an HTF-EMA-style script making ~15 IANA time calls per bar at global scope; Node ICU):
_timestampFromIANAgetDatePartsInTimezoneWhy memoizing the formatter is only half of it
Two costs are stacked, and fixing one leaves most of the time on the table:
new Intl.DateTimeFormat(...)formatter.formatToParts(date)Memoizing the formatter instance (they're pure functions of
(locale, options)per ECMA-402) removes the 41 µs and takes the execute 33s → 26s — but timezone math is still 40% of the profile, becauseformatToPartsremains per call.Caching the resolved offset removes that too: 26s → 15s, with timezone functions dropping to 2% of the profile. Overall 33s → 15s.
src/namespaces/tzOffset.tsis the shared primitive both hot paths reduce to.How the cache stays exact
An IANA offset is piecewise constant in UTC and the pieces are months long. The cache stores the offset at UTC-midnight boundaries:
formatToPartspath.So the cache never approximates across a transition. Adjacent days share a boundary probe, which means one
formatToPartsper calendar day regardless of bar interval — strictly cheaper than uncached even on a daily chart (one probe per bar versus one per call).One assumption, stated in the module header: no zone transitions twice within a single UTC day and returns to the same offset. No entry in the IANA database does this.
An hour-bucketed cache would be simpler and is wrong —
Australia/Lord_Howeshifts 30 minutes at an instant that isn't on a UTC hour boundary. It's in the test matrix for exactly that reason.Behaviour
_timestampFromIANAkeeps its single-pass semantics exactly (a wall-clock time inside a DST gap or fold is genuinely ambiguous, and Pine resolves it this way) — only the offset lookup changed.getDatePartsInTimezonenow derivesdayOfWeekarithmetically instead of parsing a localized weekday string. Same result, one less locale dependency.Tests
tests/namespaces/timezone-offset.test.tsusesIntlitself as the oracle and sweeps every hour of eight transition days across six zones —America/New_York,Europe/London,Australia/Sydney,Asia/Kolkata,Asia/Kathmandu,Australia/Lord_Howe— plus minute resolution across the US spring-forward instant, fixed-offset spellings, and the unknown-zone fallback. One case asserts the cached offset equals the uncached offset on a transition day and that the day genuinely straddles a change, so it can't pass vacuously.tests/namespaces/timezone-perf.test.tsis a regression guard: 35k bars × 5 IANA calls per bar, bounded at 2000ms. ~2890ms unfixed, ~440ms fixed.Negative control: replacing the boundary comparison with an unconditional day-start offset fails 3 of the 6 correctness cases.
Suite status
mainand this branch have the same 139 pre-existing failures:main(beacd58)The +8 are the tests added here. I haven't touched the pre-existing failures — happy to look at them separately if useful.
Notes for review
src/namespaces/Core.tsalready failsprettier --checkonmain, so I deliberately did not run--writeover it — that would have buried this diff in unrelated reformatting. The new files are prettier-clean and my edits add no new violations.