Skip to content

CI validation: CJK font setting (PR 2 mirror) - #2

Closed
tonite31 wants to merge 3 commits into
mainfrom
feat/terminal-cjk-font-setting
Closed

tonite31 wants to merge 3 commits into
mainfrom
feat/terminal-cjk-font-setting

Conversation

@tonite31

Copy link
Copy Markdown
Owner

Fork-internal PR to run the check matrix before the upstream PR leaves draft. Not for merge.

tonite31 and others added 3 commits August 11, 2026 15:42
A pane rendering Hangul would garble mid-screen and stay that way until an
unrelated window resize repaired it.

The WebGL renderer caches rasterized glyphs in fixed-size atlas pages, and the
GPU can bind only so many at once. ASCII fills one page; a CJK viewport needs
hundreds of double-width glyphs keyed by character *and* color, so it hits the
page limit and forces a merge that renumbers every cached glyph. WebglRenderer
rebuilds the model when that happens, up to 32 times per frame — but each rebuild
rasterizes again and can trip another merge, so a CJK viewport never converges.
The loop then gives up and paints a model half-built against page indexes the
last merge invalidated. Nothing marks those rows dirty again, so the garble
persists until a resize forces a full rebuild.

- Request another frame when the retry budget is exhausted, capped at 4
  consecutive frames so an oversubscribed atlas cannot spin the render loop.
- Raise the atlas page size to 1024px: 4x the glyphs per page, so a CJK viewport
  fits the texture budget and the merge path stays cold.

Also add CJK entries to the terminal font chain. Every font in it was Latin-only,
so Hangul fell to a proportional system face whose advance is not two cells wide
and drifted out of the grid. Coding faces with exact dual-width metrics come
first, then platform defaults native-before-foreign, each listed under both its
English and localized family name — a CJK-locale OS registers these faces under
the localized name only. The chain moves to lib/terminal-font-family.ts, which
folds in the second, already-drifted copy the pane defaults carried.
Two changes to how Hangul/Kana/Han resolve in a terminal pane.

The built-in chain now prefers the platform's own Korean face over a foreign one
that merely happens to be installed. Neither Apple SD Gothic Neo nor Malgun
Gothic has exact dual-width metrics, so there was no reason for a Windows font
to outrank the macOS default — but with Malgun ahead, a Mac with Office
installed rendered Hangul differently from one without.

The chain is still code-only, though, and a machine with none of the dual-width
coding faces installed had no way to reach a better one: the Font Family picker
sets the primary font, which never answers for CJK. Add terminalCjkFontFamily,
surfaced as a CJK Font Family row in the Terminal typography Advanced
disclosure, reusing the installed-font autocomplete the primary picker uses.

The chosen font sits behind the Nerd Fonts rather than at the front of the
chain, so it answers for CJK without claiming the PUA glyphs Powerline prompts
draw from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s real

The font pickers showed a five-entry list that looks like the real thing but is
the hardcoded fallback: system_profiler SPFontsDataType takes ~20s on a Mac with
a large catalog (285 families here), and until it lands the renderer shows its
curated placeholder with no sign that anything is still loading. Warm the
main-process cache two seconds after the window shows, so the list is ready long
before settings can be opened. Failures still fall back as before.
@tonite31

Copy link
Copy Markdown
Owner Author

CI validation complete: 45 checks, sole failure is the fork-irrelevant community-PR labeler (missing upstream secrets). Upstream PR stablyai#13779 is now ready for review.

@tonite31 tonite31 closed this Aug 11, 2026
tonite31 pushed a commit that referenced this pull request Aug 17, 2026
…vivor as the crasher (stablyai#14662)

* fix(crash-reporting): keep a pre-gone process-metrics sample so the crashed process's working set survives its crash report

* feat(crash-reporting): renderer peak/private bytes and gone-time system memory in crash details

* fix(crash-reporting): macOS system-memory fields and an era-invariant pin for peak/private metrics

* test(crash-reporting): kill five mutation survivors in the pre-gone sampler coverage

Adversarial review found these mutations survived the suite:
- dropping the immediate sample at startPreGoneProcessMetricsSampling()
- removing the double-start idempotence guard
- widening renderer peak/private aggregation to all buckets
- a failed sweep erasing the previous good sample
- the recorder hardcoding 'renderer' instead of event.processType

Each now has a binding assertion; also documents that the
crashed-process-absent flag is bucket-level only.

* fix(crash-reporting): prove crasher absence by vanished pid, not bucket count alone

The absent flag was bucket-level, so any surviving same-type process (a
webview guest, the dashboard popout, another utility) silently cleared it
— and webviewTag guests make multi-renderer sessions the norm. The pre-gone
sample now keeps per-process pid/bucket/workingSet identities; a sampled
same-bucket pid missing from the live set proves absence and reports the
vanished process's own working set (processMetricsVanished*), so the
crasher's size is no longer summed with surviving guests.

Also: split gone-time system memory into its own module (max-lines), pin
peak/private aggregation as a true max, clamp garbage negative working
sets and backwards clocks, pin live-metrics precedence over incoming
detail keys, and verify the sampler timer is unref'd by behavior.

* fix(crash-reporting): bucket-aware vanished-pid check with consume-once attribution

Loop-3 hardening of the vanished-pid logic:
- Live pids now carry their bucket: a recycled pid living on as a different
  process type still reads as a vanished sampled process (the bare pid set
  misread the crasher as alive).
- Vanished pids are attributed once. In a crash loop with no sweep between
  deaths, record #2 confidently inherited the FIRST crasher's pid and
  working set (dedupe window is only 2s, so both records ship); it now
  degrades to the honest bucket-count arm instead.
- An ambiguous multi-process VanishedWorkingSetMB sum is bounded by
  VanishedLargestWorkingSetMB so no single-process reading of the sum
  survives triage.
- Killer tests for the remaining mutation survivors: unreadable gone-time
  metrics prove nothing (flag/vanished stay off), pid-less sampled metrics
  never vanish, fractional-MB rounding, negative system-memory clamp, and
  full per-family precedence over colliding incoming detail keys.

* fix(crash-reporting): flag consumed and blind-era vanished attribution instead of going silent

Loop-4 hardening of the consume-once attribution:
- processMetricsVanishedAlreadyReportedCount: a record whose vanished pids
  were consumed by a prior report now says so, instead of being
  indistinguishable from "nothing vanished" while its PreGone mirrors still
  show the prior crasher's era.
- processMetricsVanishedAmbiguousWithEarlierCrash: consume-once only consumed
  when the gone-time read succeeded; a crash recorded blind (getAppMetrics
  threw) left its pid unconsumed, so the next record in the same era
  confidently emitted THAT crash's pid and working set as its own. Blind
  buckets now taint the era until a fresh sweep.
- Pin two behaviors that were correct but unpinned: a failed sweep must not
  clear attribution state, and an ambiguous vanished pair is consumed too.
- Document gone-time system memory reading healthier than at kill time, and
  the bound on the attribution set.
- Split the suppressed-breadcrumb builder into its own module (max-lines).

* fix(crash-reporting): extend vanished ambiguity to consumed eras and pin two unpinned behaviors

Loop-5 findings:
- processMetricsVanishedAmbiguousWithEarlierCrash fired only for the
  blind era; a partially-consumed era has the same shape (an earlier
  crash's unsampled respawn is as plausible a crasher as the newly
  vanished pid), yet emitted a confident VanishedPid with no flag.
- Pin the > largest tie-break (first-enumerated wins) instead of
  re-accepting it as an equivalent mutant every loop.
- The suppressed-breadcrumb type field had zero coverage after the
  module split — removing the whole block passed the suite.

* refactor(crash-reporting): cut per-pid vanished attribution, keep the stateless absence proof

Five review loops found defects in the same subsystem: the per-pid
vanished attribution outputs (consume-once set, blind-era taint,
consumed-era ambiguity). The absence proof they fed does not need any
of it — a sampled same-bucket pid missing from the live enumeration
(including cross-bucket pid recycle) is stateless and idempotent, so
it stays true for every record of a crash loop with zero module-level
attribution state.

Dropped: processMetricsVanished{Count,WorkingSetMB,Pid,
LargestWorkingSetMB,AlreadyReportedCount,AmbiguousWithEarlierCrash},
attributedVanishedPids, metricsBlindCrashBuckets, and the tests that
existed only to defend them.

Kept and still pinned: the 60s pre-gone sampler and its lifecycle,
PreGone* mirrors + SampleAgeMs, renderer peak/private, gone-time
system memory, the recorder processType binding, the live/PreGone era
invariant, and the browser-pane case (webview guests keep the renderer
bucket alive) that motivated the PR — now asserted via the absence
flag plus PreGone mirrors alone. Documented the two honest limits:
PreGone values are sample-time (up-to-60s understatement, bounded by
AgeMs and lifetime peaks), and same-bucket pid recycle inside the
sweep window is a false negative for the absence proof.

* test(crash-reporting): pin PreGoneLargest to the crasher's own size, not the bucket's running sum

Loop-6 mutation battery found one survivor in the cut's re-anchored
suite: mutating Largest to carry the bucket's running sum survived every
test, because no fixture put a same-bucket sibling BEFORE the largest
process. That is the summing-bug family loop 2 found live. The
webview-guest test now enumerates the guest first and asserts
PreGoneLargest{Pid,Type,WorkingSetMB} carry the crasher's individual
4380, alongside the 4680 bucket total.

Also restores the false-positive caveat the cut's comment dropped: a
legitimately closed sampled process can trip the absence flag if the
crasher's row somehow survives the live enumeration (pre-existing,
unchanged by the cut).

* fix(crash-reporting): mark pre-gone attribution ambiguous

PreGone mirrors are whole-app snapshots, so a larger surviving Tab can own Largest and renderer-wide peak/private fields. Emit an explicit ambiguity boundary, prove the counterexample, and use Electron's pid plus creationTime identity to catch same-bucket PID reuse without adding stateful attribution.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant