Skip to content

Commit 6b43e73

Browse files
authored
Merge pull request #70 from pineforge-4pass/fix/ki35-tzset-notifyd-storm
fix(session_time): kill per-call tzset()->notifyd storm on macOS (KI-35)
2 parents b835ae9 + eab8676 commit 6b43e73

2 files changed

Lines changed: 63 additions & 3 deletions

File tree

include/pineforge/session_time.hpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,21 @@ int64_t pine_time_close(int64_t bar_ms,
3636
// Convert "HHMM" string to minutes-since-midnight. Returns -1 on parse error.
3737
int hhmm_to_minutes(const std::string& hhmm);
3838

39+
// Pine time/date extraction from a Unix-ms bar timestamp in timezone `tz`.
40+
// Value-identical to Pine hour()/minute()/dayofweek()/... (dayofweek 1=Sun..7=Sat,
41+
// month 1..12, year full). Codegen routes hour()/minute()/... through these
42+
// instead of an inline setenv+tzset lambda, which stops the per-call macOS
43+
// tzset()->notifyd IPC storm (KI-35): UTC uses tzset-free gmtime_r, other zones
44+
// use the cached ScopedTimezone. DST-correct (localtime_r via the tz database).
45+
int pine_hour(int64_t bar_ms, const std::string& tz);
46+
int pine_minute(int64_t bar_ms, const std::string& tz);
47+
int pine_second(int64_t bar_ms, const std::string& tz);
48+
int pine_dayofmonth(int64_t bar_ms, const std::string& tz);
49+
int pine_dayofweek(int64_t bar_ms, const std::string& tz);
50+
int pine_month(int64_t bar_ms, const std::string& tz);
51+
int pine_year(int64_t bar_ms, const std::string& tz);
52+
int pine_weekofyear(int64_t bar_ms, const std::string& tz);
53+
3954
// True when local_tm falls within any of the comma-separated HHMM-HHMM
4055
// windows in windows_body. "24x7" or empty always returns true.
4156
bool local_time_in_session_windows(const std::string& windows_body,

src/session_time.cpp

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,54 @@ int hhmm_to_minutes(const std::string& hhmm) {
2525
return h * 60 + m;
2626
}
2727

28+
// Decompose a Unix-ms timestamp into local calendar fields for `tz` WITHOUT the
29+
// per-call setenv/tzset churn that made hour()/minute()/... pathologically slow
30+
// on macOS (each changed-TZ tzset() does a notifyd Mach-IPC round-trip ~173us;
31+
// ~888k/run over the full feed -> minutes of apparent "hang", KI-35). UTC needs
32+
// no tz -> tzset-free gmtime_r; other zones use the CACHED pine_tz::ScopedTimezone
33+
// (skips tzset when the active zone is unchanged, and never restores), so a run
34+
// that stays on one zone pays a single tzset. DST stays exact: localtime_r
35+
// consults the tz database exactly as the old inline lambda did.
36+
static void decompose_ms_local(int64_t bar_ms, const std::string& tz, struct tm& out) {
37+
time_t secs = static_cast<time_t>(bar_ms / 1000);
38+
const std::string t = normalize_timezone_for_posix(tz);
39+
if (t.empty() || t == "UTC" || t == "Etc/UTC") {
40+
gmtime_r(&secs, &out);
41+
} else {
42+
pine_tz::ScopedTimezone guard(t);
43+
localtime_r(&secs, &out);
44+
}
45+
}
46+
47+
// Pine time/date extraction — value-identical to the codegen's former inline
48+
// setenv+tzset lambda (pineforge-codegen tables.py TIME_FIELD_EXPRS) but
49+
// churn-free. hour()/minute()/... route through these instead of open-coding it.
50+
int pine_hour(int64_t bar_ms, const std::string& tz) { struct tm t; decompose_ms_local(bar_ms, tz, t); return t.tm_hour; }
51+
int pine_minute(int64_t bar_ms, const std::string& tz) { struct tm t; decompose_ms_local(bar_ms, tz, t); return t.tm_min; }
52+
int pine_second(int64_t bar_ms, const std::string& tz) { struct tm t; decompose_ms_local(bar_ms, tz, t); return t.tm_sec; }
53+
int pine_dayofmonth(int64_t bar_ms, const std::string& tz) { struct tm t; decompose_ms_local(bar_ms, tz, t); return t.tm_mday; }
54+
int pine_dayofweek(int64_t bar_ms, const std::string& tz) { struct tm t; decompose_ms_local(bar_ms, tz, t); return t.tm_wday + 1; }
55+
int pine_month(int64_t bar_ms, const std::string& tz) { struct tm t; decompose_ms_local(bar_ms, tz, t); return t.tm_mon + 1; }
56+
int pine_year(int64_t bar_ms, const std::string& tz) { struct tm t; decompose_ms_local(bar_ms, tz, t); return t.tm_year + 1900; }
57+
int pine_weekofyear(int64_t bar_ms, const std::string& tz) { struct tm t; decompose_ms_local(bar_ms, tz, t); return (t.tm_yday + 7 - ((t.tm_wday + 6) % 7)) / 7; }
58+
59+
static int64_t calendar_day_open_local_ms_tz(int64_t bar_ms, const std::string& tz);
60+
2861
int64_t calendar_day_open_local_ms(int64_t bar_ms, const std::string& tz) {
62+
// UTC needs no tzset: local midnight is exact integer floor. Avoiding
63+
// ScopedTimezone(UTC) here keeps the process TZ from flipping to UTC every
64+
// bar (which would re-slow the strategy's hour()/minute() zone) — see KI-35.
65+
{
66+
const std::string t = normalize_timezone_for_posix(tz);
67+
if (t.empty() || t == "UTC" || t == "Etc/UTC") {
68+
time_t secs = static_cast<time_t>(bar_ms / 1000);
69+
return static_cast<int64_t>((secs / 86400) * 86400) * 1000;
70+
}
71+
}
72+
return calendar_day_open_local_ms_tz(bar_ms, tz);
73+
}
74+
75+
static int64_t calendar_day_open_local_ms_tz(int64_t bar_ms, const std::string& tz) {
2976
pine_tz::ScopedTimezone guard(tz);
3077
time_t secs = static_cast<time_t>(bar_ms / 1000);
3178
struct tm local_tm {};
@@ -534,10 +581,8 @@ int64_t pine_time_tradingday(int64_t bar_ms,
534581
std::string eff_tz = tz.empty() ? "UTC" : tz;
535582
int bar_local_min;
536583
{
537-
pine_tz::ScopedTimezone guard(eff_tz);
538-
time_t secs = static_cast<time_t>(bar_ms / 1000);
539584
struct tm local_tm {};
540-
localtime_r(&secs, &local_tm);
585+
decompose_ms_local(bar_ms, eff_tz, local_tm); // gmtime_r for UTC (no TZ flip)
541586
bar_local_min = local_tm.tm_hour * 60 + local_tm.tm_min;
542587
}
543588

0 commit comments

Comments
 (0)