Skip to content

Commit 24e76da

Browse files
feat(Sky): Optimize early paint, add PostHog throttling, fix lifecycle phase signaling
Three optimizations for the Sky frontend: **Early paint flash elimination (Base.astro)** Add inline CSS in the `<head>` that sets `#1e1e1e` background and `#d4d4d4` text before any external CSS loads. This prevents the white→purple→dark flash visible on every Tauri `Window.navigate()` reload by matching VS Code Dark+ theme during the first paint. **Client-side PostHog throttling (PostHogBridge.ts)** Add per-event-name throttle (10s window, 2× max events/sec limit) before calling PostHog's `capture`. Dropped events are tracked and reported as a single `land:sky:throttle-dropped` event per window so volume loss is visible in the dashboard. Errors pass through unthrottled since they're rare and signal-rich. **Lifecycle phase signaling fix (Mountain.astro)** Move phase advancement (3→4) to a separate `<script>` block that executes BEFORE the workbench import, preventing it from being blocked or wiped by folder-open navigates. Fix the Tauri invoke signature from `mountain_ipc_invoke({command, args})` to `MountainIPCInvoke({method, params})` to match Mountain's actual API — the old signature caused silent deserialization failures and fell back to 8s/23s timers. Adjust timing: phase 3 fires immediately, phase 4 at 1500ms. These changes eliminate the early paint flash, surface PostHog drop metrics, and ensure Mountain receives correct phase signals without relying on fallback timers.
1 parent 8e1f50a commit 24e76da

4 files changed

Lines changed: 174 additions & 44 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ presentation.
102102
| **`Astro` Components** | Declarative UI building blocks composing the editor interface, from activity bar to status bar. |
103103
| **`Tauri` Webview** | Runtime environment where `Sky` executes, providing access to `Tauri` APIs and OS integration. |
104104
| **`Wind` Integration** | Consumes `Wind`'s `Effect-TS` services for file operations, dialogs, configuration, and state management. |
105-
| **Workbench Variants** | Three approaches (A1A3) for loading `VS Code`'s core editor components: Browser, Mountain (recommended), and Electron. |
105+
| **Workbench Variants** | Three approaches (A1-A3) for loading `VS Code`'s core editor components: Browser, Mountain (recommended), and Electron. |
106106
| **Page Routing** | Manages navigation between `index` (default), `Browser`, `BrowserProxy`, `Electron`, `Mountain`, and `Isolation` pages. |
107107
| **Event Handling** | Listens for `Tauri` events from `Mountain` to update UI state (terminal output, SCM updates, configuration changes). |
108108

Source/Function/Markup/Base.astro

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,24 @@ import Meta from "../Meta.astro";
55
<!doctype html>
66
<html lang="en" class="no-js" dir="ltr">
77
<head>
8+
<!-- Early paint colour. Without this the WKWebView renders its
9+
default white → VS Code's theme CSS → native-titlebar repaint
10+
all in ~200 ms, visible as a purple/fuchsia/white flash on
11+
every Tauri `Window.navigate()` reload (reported as
12+
"overlaying purple windows" + stuttering). Matching VS Code
13+
Dark+ (`#1e1e1e` editor bg, `#d4d4d4` editor fg) keeps the
14+
first paint inside the app's dark band so the flash
15+
disappears. Kept inline so it applies before any external
16+
CSS loads. -->
17+
<style is:inline>
18+
html,
19+
body {
20+
background: #1e1e1e;
21+
color: #d4d4d4;
22+
margin: 0;
23+
}
24+
</style>
25+
826
<script>
927
document.documentElement.classList.remove("no-js");
1028

Source/Workbench/Electron/PostHogBridge.ts

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,86 @@ interface BufferedMark {
146146
Detail: unknown;
147147
}
148148

149+
// Client-side throttle. posthog-js's CDN `array.js` ignores the
150+
// `rate_limiting` config option; its internal limiter caps at
151+
// 10 events per event-name per 10 s and silently drops overflow
152+
// with the `ignored due to client rate limiting` console warning.
153+
// Dropping here instead means the warning never fires AND the
154+
// PostHog dashboard gets a single "dropped=N" reporting event
155+
// per window so volume loss is visible.
156+
const ThrottleWindowMs = 10_000;
157+
const ThrottleLimitPerName = Math.max(1, PostHogMaxEventsPerSecond * 2);
158+
const ThrottleCounters = new Map<string, { Count: number; ResetAt: number }>();
159+
const ThrottleDropped = new Map<string, number>();
160+
161+
const ShouldThrottle = (Name: string): boolean => {
162+
const Now = Date.now();
163+
const Entry = ThrottleCounters.get(Name);
164+
if (!Entry || Entry.ResetAt <= Now) {
165+
ThrottleCounters.set(Name, {
166+
Count: 1,
167+
ResetAt: Now + ThrottleWindowMs,
168+
});
169+
return false;
170+
}
171+
Entry.Count += 1;
172+
if (Entry.Count > ThrottleLimitPerName) {
173+
ThrottleDropped.set(Name, (ThrottleDropped.get(Name) ?? 0) + 1);
174+
return true;
175+
}
176+
return false;
177+
};
178+
179+
const DrainThrottleMetrics = (PH: any): void => {
180+
if (ThrottleDropped.size === 0) return;
181+
const Summary: Record<string, number> = {};
182+
for (const [Name, Count] of ThrottleDropped.entries()) {
183+
Summary[Name] = Count;
184+
}
185+
ThrottleDropped.clear();
186+
// Single event per window - counted as one against the
187+
// throttle itself, so always safe under the limit.
188+
try {
189+
PH.capture?.("land:sky:throttle-dropped", {
190+
$component: "sky",
191+
dropped: Summary,
192+
window_ms: ThrottleWindowMs,
193+
});
194+
} catch {}
195+
};
196+
149197
const Initialize = async (): Promise<void> => {
150-
const PH = await LoadPostHog();
151-
if (!PH) return;
198+
const Raw = await LoadPostHog();
199+
if (!Raw) return;
200+
201+
// Wrap `capture` + `captureException` with the throttle so every
202+
// consumer in this module (and anywhere else that uses the
203+
// returned `PH` handle) gets the same drop semantics.
204+
const PH: any = {
205+
...Raw,
206+
capture: (Name: string, Properties?: Record<string, unknown>) => {
207+
if (ShouldThrottle(Name)) return;
208+
return Raw.capture(Name, Properties);
209+
},
210+
captureException: (
211+
Error: unknown,
212+
Properties?: Record<string, unknown>,
213+
) => {
214+
// Errors pass through unthrottled - they're already rare
215+
// and signal-rich, and the dashboard deduplicates by
216+
// $exception_type anyway.
217+
return Raw.captureException(Error, Properties);
218+
},
219+
};
220+
221+
// Periodic drain of dropped-event counters so the PostHog
222+
// dashboard sees *that* we dropped events even when every
223+
// capture of the affected name was rate-limited.
224+
const DrainTimer = setInterval(
225+
() => DrainThrottleMetrics(Raw),
226+
ThrottleWindowMs,
227+
);
228+
(DrainTimer as unknown as { unref?: () => void }).unref?.();
152229

153230
// Per-component buffers - flushed independently
154231
const Buffers = new Map<string, BufferedMark[]>();

Source/Workbench/Mountain.astro

Lines changed: 76 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,70 @@ import TelemetryBridge from "./TelemetryBridge.astro";
1414
<NLS />
1515
<TelemetryBridge />
1616

17+
<!-- Phase Advance - fires BEFORE the workbench import so it is
18+
not blocked by the main workbench script and is not wiped
19+
by a subsequent Window.navigate() folder-open reload.
20+
21+
Mountain broadcasts each phase change over
22+
`sky://lifecycle/phaseChanged`; the workbench (once it
23+
eventually loads) subscribes and gates long-running work.
24+
If Mountain never hears from Sky, its fallback timers fire
25+
at +8s (Restored) and +23s (Eventually), which the dev log
26+
surfaces as `[Lifecycle] [Fallback] Sky did not advance
27+
within …`. That delay is harmless in the abstract, but it
28+
masks real issues and pushes everything gated on phase 3 /
29+
4 out by seconds. -->
30+
<script type="module">
31+
performance.mark("land:mountain:phase:script:start");
32+
const PhaseInvoke =
33+
globalThis.__TAURI_INTERNALS__?.invoke ??
34+
globalThis.__TAURI__?.invoke ??
35+
globalThis.__TAURI__?.core?.invoke ??
36+
null;
37+
if (!PhaseInvoke) {
38+
performance.mark("land:mountain:phase:invoke-unavailable");
39+
console.warn(
40+
"[Mountain.astro] Tauri invoke not available; phase advance skipped",
41+
);
42+
} else {
43+
// Tauri command `MountainIPCInvoke` signature is
44+
// `(method: String, params: Value)` - NOT `(command,
45+
// args)`. Mismatched param names cause silent
46+
// deserialisation failure and Mountain falls back to its
47+
// 8s / 23s phase timers. All Wind / Output / astro
48+
// diagnostic call-sites use `{ method, params }` - this
49+
// block must match.
50+
const Advance = (PhaseNumber) =>
51+
PhaseInvoke("MountainIPCInvoke", {
52+
method: "lifecycle:advancePhase",
53+
params: [PhaseNumber],
54+
}).catch((Error) => {
55+
console.warn(
56+
`[Mountain.astro] advancePhase(${PhaseNumber}) failed:`,
57+
Error,
58+
);
59+
performance.mark(
60+
`land:mountain:phase:${PhaseNumber}:error`,
61+
);
62+
});
63+
// Phase 3 = Restored: workbench DOM is attached and
64+
// first-paint is imminent. Fire early - Mountain rejects
65+
// backwards / same-phase advances, so races are safe.
66+
Advance(3).then(() =>
67+
performance.mark("land:mountain:phase:restored"),
68+
);
69+
// Phase 4 = Eventually: long tail background work can
70+
// start. Delayed so Mountain sees a monotonic 3 → 4
71+
// progression even if both invokes enqueue near-
72+
// simultaneously.
73+
setTimeout(() => {
74+
Advance(4).then(() =>
75+
performance.mark("land:mountain:phase:eventually"),
76+
);
77+
}, 1500);
78+
}
79+
</script>
80+
1781
<!-- Wind Preload -->
1882
<script type="module">
1983
performance.mark("land:mountain:preload:start");
@@ -59,56 +123,27 @@ import TelemetryBridge from "./TelemetryBridge.astro";
59123
await import(WorkbenchUrl);
60124
performance.mark("land:mountain:workbench:loaded");
61125

126+
// SkyBridge install is timed after the workbench has a
127+
// chance to mount - 1s is enough for the first-paint
128+
// DOM, before the workbench typically fires its folder-
129+
// open navigate. Phase advance lives in its own script
130+
// block above so it doesn't depend on this reaching
131+
// here.
62132
setTimeout(async () => {
63133
try {
64134
const { InstallSkyBridge } = await import(
65135
"@codeeditorland/sky/Source/Function/SkyBridge"
66136
);
67137
await InstallSkyBridge();
68138
performance.mark("land:mountain:skybridge:installed");
69-
70-
// Atom P3: signal Mountain that the workbench is
71-
// interactive so `Restored` / `Eventually` phases don't
72-
// rely on Mountain's 8s/23s fallback timers. Mountain
73-
// rejects backwards/same-phase advances so calling this
74-
// after the fallback fired is harmless. The Tauri IPC
75-
// is loaded dynamically because the workbench boot runs
76-
// in a plain `<script type="module">` context without a
77-
// pre-resolved bundle.
78-
try {
79-
const TauriModule = await import(
80-
"@tauri-apps/api/core"
81-
);
82-
await TauriModule.invoke("mountain_ipc_invoke", {
83-
command: "lifecycle:advancePhase",
84-
args: [3],
85-
});
86-
performance.mark("land:mountain:phase:restored");
87-
setTimeout(async () => {
88-
try {
89-
await TauriModule.invoke(
90-
"mountain_ipc_invoke",
91-
{
92-
command: "lifecycle:advancePhase",
93-
args: [4],
94-
},
95-
);
96-
performance.mark(
97-
"land:mountain:phase:eventually",
98-
);
99-
} catch {
100-
performance.mark(
101-
"land:mountain:phase:eventually:error",
102-
);
103-
}
104-
}, 2000);
105-
} catch {
106-
performance.mark("land:mountain:phase:error");
107-
}
108-
} catch {
139+
} catch (Error) {
140+
console.warn(
141+
"[Mountain.astro] InstallSkyBridge failed:",
142+
Error,
143+
);
109144
performance.mark("land:mountain:skybridge:error");
110145
}
111-
}, 2000);
146+
}, 1000);
112147
} catch {
113148
performance.mark("land:mountain:workbench:error");
114149
}

0 commit comments

Comments
 (0)