@teambit/workspace 1.0.1192 → 1.0.1194
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Component Loading Redesign
|
|
2
2
|
|
|
3
|
-
**Status:** Phase 1 shipped; Phase 2
|
|
4
|
-
**Last updated:** 2026-
|
|
3
|
+
**Status:** Phase 1 shipped; Phase 2 re-scoped; Phase 3's premise disproved; object-cache fix shipped (see [Status](#status))
|
|
4
|
+
**Last updated:** 2026-09-24 (code references are against `master` @ `59855b104`, §4.5 @ `9307c9c6a`; line numbers will drift)
|
|
5
5
|
|
|
6
6
|
This document is the source of truth for a multi-phase effort to simplify Bit's component-loading
|
|
7
7
|
mechanism: fewer caches, a staged (lazy) loading pipeline, a single env/aspect load planner, and a
|
|
@@ -67,7 +67,9 @@ Problems:
|
|
|
67
67
|
|
|
68
68
|
- The workspace cache key embeds serialized `loadOpts`
|
|
69
69
|
(`createComponentCacheKey`, `workspace-component-loader.ts:990`), so the same component loaded
|
|
70
|
-
with different options is cached
|
|
70
|
+
with different options is cached under separate keys. _(Measured 2026-09-22: the keys point to
|
|
71
|
+
the same object, so this costs no memory — the real issue is that a key describes the request,
|
|
72
|
+
not what was loaded. See §4.3 item 2.)_
|
|
71
73
|
- Invalidation requires three coordinated calls (`workspace.ts:829-841`:
|
|
72
74
|
`componentLoader.clearCache` + legacy `clearComponentsCache` + `componentStatusLoader.clearCache`).
|
|
73
75
|
- Cache hits are silent — a stale cache is indistinguishable from a fresh load in the logs.
|
|
@@ -204,14 +206,30 @@ earlier ones teach us).
|
|
|
204
206
|
### Phase 2 — Quick perf wins on existing seams
|
|
205
207
|
|
|
206
208
|
- [x] Benchmark harness committed + baseline recorded (see §4) — **gate for the rest of the phase**
|
|
207
|
-
- [
|
|
209
|
+
- [x] ~~Forward `loadOpts` in the batch `executeLoadSlot` path~~ — **implemented, measured, rejected.**
|
|
210
|
+
Halved `on-load` aggregate self-time with **no wall change** and **+67MB RSS** (§4.3). Not merged.
|
|
211
|
+
- [ ] ~~Default `loadDocs: false, loadCompositions: false` for non-UI flows~~ — **blocked**: `status`
|
|
212
|
+
already sets them; they can't take effect until the cache stops keying on them (Phase 3)
|
|
213
|
+
- [ ] Lazy file contents in `ModelComponent.toConsumerComponent` (targets `graph`, and the RSS trend)
|
|
208
214
|
- [ ] `bit deps usage`: ids + stored deps instead of full load
|
|
209
215
|
- [ ] IDE metadata endpoint (`api-for-ide.ts`): S0-S2-level data only
|
|
210
216
|
- [ ] `bit remove` / forking: drop full-component loads where only ids/paths are used
|
|
211
|
-
|
|
217
|
+
|
|
218
|
+
**Explicitly dropped from this phase:** narrowing the deps-cache freshness fs scan (PR #10445,
|
|
219
|
+
closed 2026-09-22). It cut warm `status` fs syscalls 37% (74.3k → 46.4k) but moved wall ~0.3s —
|
|
220
|
+
the syscalls are `stat`/`lstat`/`readdir`, which overlap with CPU on a warm SSD — in exchange for
|
|
221
|
+
narrowing the invalidation signal of the highest-blast-radius cache in the loader. Bad trade. The
|
|
222
|
+
useful output of that work is the §4.1/§4.2 profiling and the node_modules invalidation constraints
|
|
223
|
+
recorded in the deps-cache notes.
|
|
212
224
|
|
|
213
225
|
### Phase 3 — Cache consolidation
|
|
214
226
|
|
|
227
|
+
> **Reassessed 2026-09-22 — not a perf lever.** Its promotion (§4.3) rested on the component cache
|
|
228
|
+
> holding duplicate copies; measured, it holds exactly one object per id (§4.3 item 2, §4.5). What
|
|
229
|
+
> remains is simplification and a correctness hazard (keys describe the request, not the content),
|
|
230
|
+
> worth doing only alongside partial-loading work that needs it. The 500-component cap on these
|
|
231
|
+
> caches is the one open perf question here (§4.5).
|
|
232
|
+
|
|
215
233
|
- [ ] Introduce `ComponentCacheManager` (unused, with tests)
|
|
216
234
|
- [ ] Migrate the workspace loader's four caches onto it
|
|
217
235
|
- [ ] Migrate scope loader + legacy ComponentLoader caches; single `invalidate(id, reason)` event
|
|
@@ -250,18 +268,52 @@ The `Peak RSS` column is the worst case across the four commands (`bit graph`).
|
|
|
250
268
|
median seconds. Per-command peak RSS for the baseline: status 1237MB, list 425MB, show 253MB,
|
|
251
269
|
graph 2013MB.
|
|
252
270
|
|
|
253
|
-
| Milestone
|
|
254
|
-
|
|
|
255
|
-
| Baseline (pre-Phase 2)
|
|
256
|
-
|
|
|
257
|
-
|
|
|
258
|
-
| After Phase
|
|
259
|
-
| After Phase
|
|
271
|
+
| Milestone | `bit status` | `bit list` | `bit show <comp>` | `bit graph --json` | Peak RSS |
|
|
272
|
+
| ------------------------ | ------------ | ---------- | ----------------- | ------------------ | -------- |
|
|
273
|
+
| Baseline (pre-Phase 2) | 11.24s | 1.59s | 1.73s | 20.49s | 2013MB |
|
|
274
|
+
| Re-baseline (2026-09-22) | 9.43s | 1.72s | 1.23s | 18.59s | 2352MB |
|
|
275
|
+
| 2026-10-01 (§4.5-4.7) | 6.80s | 1.57s | 1.19s | 19.63s | 2495MB |
|
|
276
|
+
| After Phase 2 | — | — | — | — | — |
|
|
277
|
+
| After Phase 3 | — | — | — | — | — |
|
|
278
|
+
| After Phase 4 | — | — | — | — | — |
|
|
279
|
+
| After Phase 5 | — | — | — | — | — |
|
|
260
280
|
|
|
261
281
|
Baseline measured 2026-06-15 on darwin (Apple silicon), `bit` @ 1.13.222, node v22.20.0,
|
|
262
282
|
`bit show teambit.workspace/workspace`. The two clear hotspots are `bit status` (11s) and
|
|
263
283
|
`bit graph` (20s, 2GB).
|
|
264
284
|
|
|
285
|
+
**Re-baseline 2026-09-22** — same machine/method, `bit` @ 2.2.57, node v22.20.0. Note the workspace
|
|
286
|
+
grew from ~313 to **331 components** (+5.8%), so per-component deltas are better than the raw wall
|
|
287
|
+
numbers suggest. Per-command peak RSS: status 1239MB, list 549MB, show 260MB, graph 2352MB.
|
|
288
|
+
|
|
289
|
+
| metric | baseline | re-baseline | raw Δ | per-component Δ |
|
|
290
|
+
| ----------------- | -------- | ----------- | ------ | --------------- |
|
|
291
|
+
| `bit status` wall | 11.24s | 9.43s | −16.1% | −20.6% |
|
|
292
|
+
| `bit graph` wall | 20.49s | 18.59s | −9.3% | −14.2% |
|
|
293
|
+
| `bit show` wall | 1.73s | 1.23s | −28.9% | n/a (1 comp) |
|
|
294
|
+
| `bit list` wall | 1.59s | 1.72s | +8.2% | +2.3% (flat) |
|
|
295
|
+
| `bit graph` RSS | 2013MB | 2352MB | +16.8% | +10.4% |
|
|
296
|
+
| `bit list` RSS | 425MB | 549MB | +29.2% | +22.2% |
|
|
297
|
+
|
|
298
|
+
Two things to read from this:
|
|
299
|
+
|
|
300
|
+
- **The wall-time gains came from outside this effort.** No Phase-2 item shipped, yet `status` and
|
|
301
|
+
`graph` improved materially — from targeted fixes landed independently (batching/deduping
|
|
302
|
+
dependency imports when building graphs from fs, dropping a per-invocation hard-link-directory
|
|
303
|
+
load, unifying component load paths). The redesign plan is not what moved these numbers.
|
|
304
|
+
- **Memory is trending the wrong way.** `graph` peak RSS is up 339MB and `list` up 124MB, beyond
|
|
305
|
+
what the +5.8% component growth explains. No phase currently targets memory; §2.1 lazy file
|
|
306
|
+
contents is the closest lever.
|
|
307
|
+
|
|
308
|
+
**2026-10-01**, after #10723 and §4.6-4.7, same machine/method, ~333 components. Per-command peak
|
|
309
|
+
RSS: status 927MB, list 546MB, show 249MB, graph 2495MB. Against the September re-baseline:
|
|
310
|
+
|
|
311
|
+
- `status` 9.43s → 6.80s (**−28%**) and 1239MB → 927MB (**−25%**).
|
|
312
|
+
- `list` −9%, `show` −3%.
|
|
313
|
+
- `graph` is **+6% wall and +6% RSS** (18.59s → 19.63s, 2352MB → 2495MB). The machine's load rose
|
|
314
|
+
during its runs (it went last), so some of this may be noise, but nothing here targeted it. `graph`
|
|
315
|
+
is now the outlier on both axes; its dominant cost is still file-content reads (§4.1).
|
|
316
|
+
|
|
265
317
|
### 4.1 Profiling findings (where the warm load time actually goes)
|
|
266
318
|
|
|
267
319
|
Beyond wall-time, an opt-in aggregate profiler (`BIT_LOAD_PROFILE=1 bit <cmd>`, in
|
|
@@ -303,18 +355,258 @@ Numbers below are aggregate self-time across ~313 components at ~6× concurrency
|
|
|
303
355
|
`deps usage`/IDE/`remove`/forking; `loadDocs/loadCompositions: false` trims `status`'s slot work
|
|
304
356
|
(~1.7s). The big `status` number is deferred to the staged-loading phase.
|
|
305
357
|
|
|
358
|
+
### 4.2 Re-profile 2026-09-22 (`bit status`, 331 components, 9.43s wall)
|
|
359
|
+
|
|
360
|
+
`BIT_LOAD_PROFILE=1 bit status`, aggregate self-time 68.1s (wall was 9.43s — the sum exceeds wall
|
|
361
|
+
because concurrently-interleaved spans each count their own `await` time).
|
|
362
|
+
|
|
363
|
+
> **Do not read these as wall-time budgets.** §4.3 shows, by direct experiment, that cutting a row
|
|
364
|
+
> here in half changed wall by nothing. Use this table to find _where to look_, never to size a win.
|
|
365
|
+
|
|
366
|
+
| stage | self-time | calls | share of self-time |
|
|
367
|
+
| -------------------------------------------- | --------- | ----- | ------------------ |
|
|
368
|
+
| `legacy-load-deps` | 34.3s | 331 | 50% |
|
|
369
|
+
| `on-load` (docs, compositions, schema, pkg…) | 13.7s | 2317 | 20% |
|
|
370
|
+
| `dependency-resolution` | 6.7s | 331 | 10% |
|
|
371
|
+
| `workspace.get` | 5.5s | 3349 | 8% |
|
|
372
|
+
| `execute-load-slot` (own) | 4.5s | 331 | 7% |
|
|
373
|
+
| `consumer-fs-load` | ~0s self | 7 | — |
|
|
374
|
+
|
|
375
|
+
Observations:
|
|
376
|
+
|
|
377
|
+
- `legacy-load-deps` is **50% of all load self-time** — still dependency-object materialization on
|
|
378
|
+
cache _hit_, not resolution. Per-component it improved (137ms → 104ms, −24%) but it did not change
|
|
379
|
+
shape. Same picture as June, three months apart.
|
|
380
|
+
- `on-load` is **20%** across **2317 calls** (331 components × ~7 handlers). `bit status` passes
|
|
381
|
+
`loadDocs:false`/`loadCompositions:false`, but they are **ineffective on the batch path**:
|
|
382
|
+
`workspace-component-loader.ts:479-480` calls `executeLoadSlot(component)` without forwarding
|
|
383
|
+
`loadOpts` (the single-component path at :1083/:1092 does forward them).
|
|
384
|
+
- `workspace.get` shows **3349 calls for 331 components (~10×)** — explained and tested in §4.4.
|
|
385
|
+
|
|
386
|
+
### 4.3 ⚠️ Aggregate self-time does not predict wall-time — validate before building
|
|
387
|
+
|
|
388
|
+
The `on-load` row above was tested directly, and the result invalidates the metric this section is
|
|
389
|
+
built on. Forwarding `loadOpts` on the batch path (the "one-line fix") was implemented and measured:
|
|
390
|
+
|
|
391
|
+
| metric | without fix | with fix | result |
|
|
392
|
+
| ------------------------------ | ----------- | ----------- | ---------------------- |
|
|
393
|
+
| `on-load` aggregate self-time | 13.7s | 6.6s | **−52%** |
|
|
394
|
+
| total load self-time | 68.1s | 53.5s | **−21%** |
|
|
395
|
+
| **`bit status` wall (median)** | **9.05s** | **8.98s** | **−0.07s — noise** |
|
|
396
|
+
| **peak RSS** | **~1228MB** | **~1295MB** | **+67MB — consistent** |
|
|
397
|
+
|
|
398
|
+
Wall is the median of 9 runs per variant, **interleaved** (A/B/A/B/A/B, recompiling between) to
|
|
399
|
+
cancel drift. Drift matters: the same unchanged code measured 9.43s and 8.99s twenty minutes apart,
|
|
400
|
+
a larger gap than the effect being tested. RSS is 4 runs per variant with zero overlap between the
|
|
401
|
+
two sets.
|
|
402
|
+
|
|
403
|
+
So halving the aggregate self-time of a stage worth "~1.9s of wall" by the divide-by-concurrency
|
|
404
|
+
estimate produced **no measurable wall change**, and cost memory. Two lessons:
|
|
405
|
+
|
|
406
|
+
1. **Aggregate self-time is a hypothesis generator, not a size estimate.** Spans are summed across
|
|
407
|
+
concurrently-interleaved async work on one JS thread, so most of what is counted is `await` time
|
|
408
|
+
overlapping other work, not critical-path CPU. Dividing by observed concurrency does **not**
|
|
409
|
+
recover wall-time. This is the third time the metric has misled this effort (the earlier "39s"
|
|
410
|
+
correction, the deps-cache fs-scan of #10445, and now this). **Do not size or prioritize work
|
|
411
|
+
from §4.1/§4.2 numbers — A/B the wall-clock first.**
|
|
412
|
+
2. ~~Forwarding `loadOpts` costs memory because the cache key embeds it.~~ **Retracted (§4.5).**
|
|
413
|
+
Instrumenting the cache showed every id does get two keys, but **both point to the same object**
|
|
414
|
+
(with or without the fix) — the partial-key entry is later mutated in place into the full
|
|
415
|
+
component. So there is no second copy; the +67MB has some other cause and is unexplained. What
|
|
416
|
+
_is_ true is subtler: the key records what the caller _asked for_, not what was loaded — `getMany`
|
|
417
|
+
stores every result under the full key (:203) regardless of its options and ignores
|
|
418
|
+
`storeInCache`. With the forwarding fix, docs-less components would have been served to later
|
|
419
|
+
full `get`s. That's a correctness hazard for partial loading, not a memory one.
|
|
420
|
+
|
|
421
|
+
**Method for the rest of this effort:** every perf claim lands with an interleaved wall-clock A/B on
|
|
422
|
+
`scripts/bench-component-loading.js` and a peak-RSS comparison. No merges justified by profiler
|
|
423
|
+
self-time alone.
|
|
424
|
+
|
|
425
|
+
### 4.4 The `workspace.get` 10× call count: explained, tested, and not worth a cache
|
|
426
|
+
|
|
427
|
+
Attributing every `workspace.get` call by caller (temporary stack-tally instrumentation, since
|
|
428
|
+
removed) on a warm `bit status`:
|
|
429
|
+
|
|
430
|
+
| caller | calls | note |
|
|
431
|
+
| ---------------------------------------------- | ----- | ------------------------------- |
|
|
432
|
+
| `envs` → `getEnvComponentByEnvId` (`opts={}`) | 2520 | resolving the **env** component |
|
|
433
|
+
| the batch load path (one per component) | 331 | expected |
|
|
434
|
+
| `component-dependency-factory` | 329 | ~one per component |
|
|
435
|
+
| `envs` (with `loadExtensions/executeLoadSlot`) | 162 | a second envs path |
|
|
436
|
+
| misc | 7 | |
|
|
437
|
+
|
|
438
|
+
**80% of all `workspace.get` calls are the envs aspect re-resolving the env component** — and this
|
|
439
|
+
workspace has **4 distinct envs**. `getEnvComponentByEnvId`
|
|
440
|
+
(`scopes/envs/envs/environments.main.runtime.ts`) does `host.get(envId)` on every call, and its
|
|
441
|
+
callers (dependency-resolver, dev-files, preview, and the env-descriptor path) each run per
|
|
442
|
+
component.
|
|
443
|
+
|
|
444
|
+
Memoizing it by `envId` was implemented and measured:
|
|
445
|
+
|
|
446
|
+
| metric | without memo | with memo | result |
|
|
447
|
+
| ----------------------------- | ------------ | --------- | -------------------------- |
|
|
448
|
+
| `workspace.get` calls | 3349 | 835 | **−75%** |
|
|
449
|
+
| `workspace.get` self-time | 5.48s | 5.09s | −0.39s — **nearly free** |
|
|
450
|
+
| `bit status` wall (median, 9) | 9.59s | 9.45s | −0.14s (−1.5%), consistent |
|
|
451
|
+
| peak RSS | ~1234MB | ~1246MB | +12MB, within noise |
|
|
452
|
+
|
|
453
|
+
**Conclusion: real but small, and the wrong fix.** Removing 75% of the calls moved
|
|
454
|
+
`workspace.get` self-time by only 0.39s, which proves those 2514 calls were already cheap cache
|
|
455
|
+
hits — the alarming 10× count was a **third red herring**, consistent with §4.3. The 1.5% wall gain
|
|
456
|
+
is genuine but would be bought by adding a 12th ad-hoc cache, with its own invalidation surface, to
|
|
457
|
+
a system whose stated core problem (§1.2) is _~11 uncoordinated caches_. That is the anti-pattern
|
|
458
|
+
this effort exists to remove.
|
|
459
|
+
|
|
460
|
+
**Where it belongs instead:** this is direct evidence for the standalone **`EnvResolver`** of §2.4 —
|
|
461
|
+
env identity is asked for constantly and should be resolvable from S0-S2 data with one owner and one
|
|
462
|
+
cache, rather than by loading a full env Component through the general loader 2682 times. Fold it
|
|
463
|
+
into Phase 5, and do not bolt on a point-memo before then.
|
|
464
|
+
|
|
465
|
+
### 4.5 A real CPU profile: a third of `status` is the object layer, and it was thrashing
|
|
466
|
+
|
|
467
|
+
The span profiler (§4.1-4.4) misled three times, so `bit status` was profiled with V8 instead
|
|
468
|
+
(`node --cpu-prof`). V8 samples measure on-thread time, which _does_ sum to wall-time (9.17s
|
|
469
|
+
sampled: 7.7s JS/native, 0.83s GC, 0.46s idle). **Use this, not `BIT_LOAD_PROFILE`, to decide
|
|
470
|
+
where to work.**
|
|
471
|
+
|
|
472
|
+
The dominant cost is not in the component loader at all: **`@teambit/objects` is 1.7s self / 3.1s
|
|
473
|
+
inclusive** — about a third of wall. Mostly `parseObject` (1.1s, Version/ModelComponent/Source
|
|
474
|
+
JSON parsing). Counting parses per hash showed **half of them were repeats**: status touches ~4,700
|
|
475
|
+
distinct objects, and the `Repository` LRU was capped at **3,000 objects**, so it evicted and
|
|
476
|
+
re-parsed ~4,000. Caching everything costs ~124MB inflated (22MB on disk).
|
|
477
|
+
|
|
478
|
+
The count cap had been lowered twice for OOM reasons (10K → 5K → 3K), and objects range from bytes
|
|
479
|
+
to ~1.7MB, so raising the count would repeat a known mistake. Fixed instead by **bounding the cache
|
|
480
|
+
by approximate inflated bytes** (256MB default) — [#10723](https://github.com/teambit/bit/pull/10723) (merged):
|
|
481
|
+
|
|
482
|
+
| interleaved A/B | old (3,000 objects) | byte budget | result |
|
|
483
|
+
| --------------------------------- | ------------------- | ----------- | --------------------- |
|
|
484
|
+
| `bit status` wall (median, 8) | 8.64s | 7.37s | **−1.27s (−15%)** |
|
|
485
|
+
| `bit status` wall range | 8.50-8.91s | 7.36-7.48s | no overlap, 8/8 pairs |
|
|
486
|
+
| `bit status` peak RSS | ~1305MB | ~1130MB | **~−170MB** |
|
|
487
|
+
| `bit graph --json` wall (3 pairs) | 20.91s | 19.94s | −0.97s (−5%) |
|
|
488
|
+
| `bit list` / `bit show` | — | — | neutral |
|
|
489
|
+
|
|
490
|
+
It wins on memory too, but not because the old cache retained duplicates: only ~78 duplicate copies
|
|
491
|
+
(2.6MB) were live at the end of the old run. The win is **churn**: each of the ~4,000 re-parses
|
|
492
|
+
inflated and parsed a fresh copy that became garbage, so the old policy did 8,702 parses vs 4,715.
|
|
493
|
+
Peak `arrayBuffers` fell ~214→~103MB, peak `heapUsed` ~840→~768MB, and GC time ~820→~635ms. This is
|
|
494
|
+
the first change in this effort that moved wall-time by more than noise — and none of the phases
|
|
495
|
+
pointed at it.
|
|
496
|
+
|
|
497
|
+
**The cache is essential, but a byte budget alone has a cliff.** With the cache disabled (`max 1`),
|
|
498
|
+
`status` takes 22.4s with 31,213 parses. A workspace whose working set exceeds the budget falls back
|
|
499
|
+
to master's thrashing, and was slightly _worse_ than master at 3×. So #10723 also added a **weak layer** (`LiveObjects`, `objects/live-objects.ts`): a
|
|
500
|
+
hash → `WeakRef` map with a `FinalizationRegistry`. On an LRU miss, it returns the object if
|
|
501
|
+
something else (e.g. a loaded component) still references it; it never keeps anything alive by
|
|
502
|
+
itself. Objects too big for the LRU (>100KB compressed) are tracked there too. Alone it's
|
|
503
|
+
insufficient (9.5s vs 7.7s: most objects are unreferenced between uses); on top of the LRU it's a no-op while
|
|
504
|
+
the budget fits and removes the cliff when it doesn't. Simulated larger workspaces (both limits
|
|
505
|
+
shrunk by the same factor, `status`, median of 2, builds alternated):
|
|
506
|
+
|
|
507
|
+
| simulated size | master | byte budget | byte budget + weak |
|
|
508
|
+
| -------------- | -------------- | -------------- | ------------------ |
|
|
509
|
+
| this repo | 8.9s / 1317MB | 7.7s / 1140MB | 7.8s / 1110MB |
|
|
510
|
+
| 3× | 10.5s / 1629MB | 11.2s / 1683MB | **8.7s / 1200MB** |
|
|
511
|
+
| 5× | 12.1s / 1934MB | 12.1s / 1874MB | **9.2s / 1303MB** |
|
|
512
|
+
|
|
513
|
+
Correctness notes for anyone touching it: a `ModelComponent`'s hash is name-based, so one hash maps
|
|
514
|
+
to different content over time — every path that writes, removes, or clears the LRU must update the
|
|
515
|
+
weak map too (`_writeOne` only after the write succeeds; `removeFromCache`; `clearObjectsFromCache`).
|
|
516
|
+
|
|
517
|
+
Other hotspots from the same profile, since acted on (§4.6, §4.7):
|
|
518
|
+
|
|
519
|
+
- `ModelComponent.versionsIncludeOrphaned`, a getter that spreads `{...versions, ...orphanedVersions}`
|
|
520
|
+
on **every access** (~375ms) → single-tag lookups no longer use it ([#10739](https://github.com/teambit/bit/pull/10739)).
|
|
521
|
+
- `getTagOfRefIfExists`, a linear scan over all tags (~150ms self) → no longer copies; still linear (§4.7).
|
|
522
|
+
- `Version.calculateHash` from `isComponentModified` (~285ms) → [#10741](https://github.com/teambit/bit/pull/10741).
|
|
523
|
+
- `status` parses ~3,300 `Source` objects (file contents from the model). The "lazy file contents"
|
|
524
|
+
item of Phase 2 would remove these parses entirely. Still open.
|
|
525
|
+
- The **component** caches are capped at **500**, and this workspace has 339 → measured in §4.6: the
|
|
526
|
+
cliff was real, and steep.
|
|
527
|
+
|
|
528
|
+
### 4.6 The component-cache cliff: eviction freed nothing and forced rebuilds
|
|
529
|
+
|
|
530
|
+
A larger workspace was simulated by shrinking the component caches' limit
|
|
531
|
+
(`BIT_CONFIG_CACHE_MAX_COMPONENTS=N`, which bounds the 8 caches sized by `cache.max.components`).
|
|
532
|
+
On master, warm `bit status`:
|
|
533
|
+
|
|
534
|
+
| limit | wall | peak RSS |
|
|
535
|
+
| ------------- | ---------- | -------- |
|
|
536
|
+
| default (500) | ~8.7s | ~1.1GB |
|
|
537
|
+
| 200 | 16.1-17.0s | 1.47GB |
|
|
538
|
+
| 50 | 29-30.8s | 1.65GB |
|
|
539
|
+
|
|
540
|
+
So a workspace a few times this size would take 2-3× longer. Isolating each cache (all but one
|
|
541
|
+
unbounded) put the cliff on the three **component** caches: the workspace loader's, the scope
|
|
542
|
+
loader's, and the legacy `ComponentLoader`'s.
|
|
543
|
+
|
|
544
|
+
The cause is a scan pattern that an LRU can't serve. `status` requests every component several times
|
|
545
|
+
over, and between passes it keeps all of them alive in its own list. Evicting a component therefore
|
|
546
|
+
frees no memory (the status list still holds it); it only makes the next request rebuild it from
|
|
547
|
+
scratch. Hence higher RSS at lower limits, not lower.
|
|
548
|
+
|
|
549
|
+
Fixed with the same idea as §4.5, applied to the component caches:
|
|
550
|
+
[#10736](https://github.com/teambit/bit/pull/10736) adds a `weak: true` option to `createInMemoryCache`
|
|
551
|
+
(`WeakValues`: a `WeakRef` map beside the LRU, cleaned by a `FinalizationRegistry`). On a miss it
|
|
552
|
+
returns the evicted value if it is still alive and puts it back in the LRU. `keys()`, `delete` and
|
|
553
|
+
`deleteAll` include the weak entries, so key-based invalidation (`clearComponentsCache` iterates
|
|
554
|
+
`keys()`) still clears them. It never keeps anything alive on its own.
|
|
555
|
+
|
|
556
|
+
| limit | master | with #10735 + #10736 |
|
|
557
|
+
| ----- | ------------------- | -------------------- |
|
|
558
|
+
| 200 | 16.1-17.0s / 1.47GB | 8.7s / 1.1GB |
|
|
559
|
+
| 50 | 29-30.8s / 1.65GB | 8.9s / 1.1GB |
|
|
560
|
+
|
|
561
|
+
At limit 50 the output is identical to the default.
|
|
562
|
+
|
|
563
|
+
**The weak layer exposed a latent bug, fixed separately in [#10735](https://github.com/teambit/bit/pull/10735).**
|
|
564
|
+
With it on the legacy cache, `status` reported 23 "failed loading env" issues. `buildLoadGroups`
|
|
565
|
+
passed each component's extension data to the next step **through** the bounded
|
|
566
|
+
`componentsExtensionsCache`. Entries evicted in between were missing, so their envs were left out of
|
|
567
|
+
the env-first load group, and components were loaded before their env was registered. On master this
|
|
568
|
+
was hidden by accidental reloads later in the command, the same rebuilds that made it slow.
|
|
569
|
+
`populateScopeAndExtensionsCache` now returns the data as a map, and the cache is only a cache again.
|
|
570
|
+
Performance-neutral on its own.
|
|
571
|
+
|
|
572
|
+
### 4.7 Follow-ups from the CPU profile: copies nobody reads
|
|
573
|
+
|
|
574
|
+
With the caches fixed, the profile was flat enough that the next items were plain wasted work:
|
|
575
|
+
|
|
576
|
+
| PR | change | measured |
|
|
577
|
+
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------- |
|
|
578
|
+
| [#10739](https://github.com/teambit/bit/pull/10739) | single-tag lookups (`getRef`, `getTagOfRefIfExists`, `sources.get/exists`) read `versions`/`orphanedVersions` instead of copying both | `status` 7.6 → 6.8s (5 interleaved rounds) |
|
|
579
|
+
| [#10741](https://github.com/teambit/bit/pull/10741) | `Version.id()` (the hash) builds only its 8 fields, not all of `toObject()`; `toConfigArray` clones only the config; ids not cloned in `toConsumerComponent` | `status` 8.5 → 7.65s, peak RSS 1.1GB → 985MB (5 rounds) |
|
|
580
|
+
| [#10742](https://github.com/teambit/bit/pull/10742) | `.bitmap` lookups index ids by name; the `IfExist` variants no longer throw and catch | time in those lookups ~140ms → ~10ms (CPU profile) |
|
|
581
|
+
|
|
582
|
+
`Version.id()` is a content hash, so its output must not change: the old and new implementations were
|
|
583
|
+
compared on all 15,398 version objects in this repo's scope, with identical results. Output of
|
|
584
|
+
`bit status` was identical in every A/B.
|
|
585
|
+
|
|
586
|
+
Checked and not worth acting on:
|
|
587
|
+
|
|
588
|
+
- **Object parsing (~590ms).** After #10723, each of the ~4,700 objects `status` touches is parsed
|
|
589
|
+
once (15 repeats, all `Source`). Real work, no longer churn.
|
|
590
|
+
- **Module loading.** `bin/bit.js` already enables Node's compile cache; the cost only shows up
|
|
591
|
+
right after a recompile.
|
|
592
|
+
|
|
593
|
+
Still open: `getHeadRegardlessOfLaneAsTagOrHash` scans all tags to find the head's tag (~145ms,
|
|
594
|
+
several callers per component). The head is usually the newest tag, so searching from the end would
|
|
595
|
+
find it at once. `extensions.clone()` in `toConsumerComponent` (~175ms) is needed, since it keeps a
|
|
596
|
+
loaded component from mutating the cached `Version`.
|
|
597
|
+
|
|
306
598
|
---
|
|
307
599
|
|
|
308
600
|
## Status
|
|
309
601
|
|
|
310
|
-
| Phase | State
|
|
311
|
-
| ----------------------- |
|
|
312
|
-
| 1 — Observability | done
|
|
313
|
-
| 2 — Quick perf wins |
|
|
314
|
-
| 3 — Cache consolidation | not started | — | —
|
|
315
|
-
| 4 — Staged pipeline | not started
|
|
316
|
-
| 5 — Env planner | not started
|
|
317
|
-
| 6 — Legacy inversion | not started
|
|
602
|
+
| Phase | State | OpenSpec change | PRs |
|
|
603
|
+
| ----------------------- | ----------------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
604
|
+
| 1 — Observability | done | `component-load-observability` | [#10418](https://github.com/teambit/bit/pull/10418) |
|
|
605
|
+
| 2 — Quick perf wins | re-scoped, 1/7 done; profile-driven fixes merged (§4.5-4.7) | — | [#10445](https://github.com/teambit/bit/pull/10445) (closed, not merged), [#10723](https://github.com/teambit/bit/pull/10723), [#10735](https://github.com/teambit/bit/pull/10735), [#10736](https://github.com/teambit/bit/pull/10736), [#10739](https://github.com/teambit/bit/pull/10739), [#10741](https://github.com/teambit/bit/pull/10741), [#10742](https://github.com/teambit/bit/pull/10742) |
|
|
606
|
+
| 3 — Cache consolidation | not started — premise disproved, demoted (§4.5) | — | — |
|
|
607
|
+
| 4 — Staged pipeline | not started | — | — |
|
|
608
|
+
| 5 — Env planner | not started | — | — |
|
|
609
|
+
| 6 — Legacy inversion | not started | — | — |
|
|
318
610
|
|
|
319
611
|
**Log:**
|
|
320
612
|
|
|
@@ -340,3 +632,58 @@ Numbers below are aggregate self-time across ~313 components at ~6× concurrency
|
|
|
340
632
|
file-content reads (`consumer-fs-load`, 5.8s) → the target for lazy file contents. (Correction: an
|
|
341
633
|
earlier "39s" figure was aggregate-concurrent self-time, not wall; warm wall is ~13s.) Direction
|
|
342
634
|
for the next Phase-2 PR intentionally left open.
|
|
635
|
+
- 2026-09-22 — **Phase 2 reassessed after a 3-month gap.** Re-baselined (§4) and re-profiled (§4.2).
|
|
636
|
+
Findings:
|
|
637
|
+
1. `status` −16% / `graph` −9% / `show` −29% since June, **with no Phase-2 item shipped** — the
|
|
638
|
+
gains came from targeted fixes landed outside this effort. Peak RSS regressed (`graph` +339MB,
|
|
639
|
+
`list` +124MB); nothing currently targets memory.
|
|
640
|
+
2. The structural conclusion is confirmed a second time: `legacy-load-deps` is now **50% of all
|
|
641
|
+
load self-time**, still materialization-on-cache-hit. Phase 4 is the lever; Phase 2 was never
|
|
642
|
+
going to move it. Phase 4 promoted to the next high-value target.
|
|
643
|
+
3. Found the concrete cheap win: `bit status` already sets `loadDocs:false`/`loadCompositions:false`
|
|
644
|
+
but the batch path drops them (`workspace-component-loader.ts:479-480` doesn't forward
|
|
645
|
+
`loadOpts`), leaving ~1.9s of `on-load` work on the table across 2317 handler calls.
|
|
646
|
+
4. New unexplained signal: `workspace.get` is called **3349× for 331 components (~10×)**.
|
|
647
|
+
5. **The cheap win was implemented and rejected on measurement** (§4.3). Forwarding `loadOpts`
|
|
648
|
+
halved `on-load` self-time but moved wall by −0.07s (noise) and cost +67MB RSS, because the
|
|
649
|
+
component cache keys on `loadDocs`/`loadCompositions` and so stores a second copy. This
|
|
650
|
+
invalidated the prioritization metric: **aggregate self-time does not predict wall-time**, for
|
|
651
|
+
the third time in this effort. Phase 4 is therefore _not_ promoted on the strength of the
|
|
652
|
+
`legacy-load-deps` 50% figure; **Phase 3 (cache consolidation) is promoted instead**, since the
|
|
653
|
+
loadOpts-in-cache-key problem blocks every partial-loading idea in Phases 2 and 4. All further
|
|
654
|
+
perf claims require an interleaved wall-clock A/B plus peak-RSS comparison.
|
|
655
|
+
6. **The `workspace.get` 10× count was chased down and also rejected** (§4.4). 80% of the 3349
|
|
656
|
+
calls are the envs aspect re-resolving one of only 4 distinct envs. Memoizing removes 75% of
|
|
657
|
+
the calls for −0.14s (−1.5%) wall — real, but it buys that by adding a 12th ad-hoc cache to the
|
|
658
|
+
~11 the effort exists to consolidate. Recorded as evidence for the §2.4 `EnvResolver` (Phase 5)
|
|
659
|
+
instead. Net result of this session: three candidate quick wins investigated, **all three
|
|
660
|
+
rejected on measurement**, and the prioritization metric itself invalidated.
|
|
661
|
+
PR #10445 closed unmerged (see §3 for the rationale). The abandoned May branch
|
|
662
|
+
`refactor/component-loading-v2-take-3` (`UnifiedComponentLoader` behind `BIT_LOADER=new`) predates
|
|
663
|
+
this document and is superseded by the phase plan — not to be resumed.
|
|
664
|
+
- 2026-09-22 — **Phase 3 started, and its premise disproved in the first hour.** Instrumenting the
|
|
665
|
+
component cache found one object per id (two keys, same object), so consolidating it saves no
|
|
666
|
+
memory; §4.3's explanation of the +67MB was wrong and is retracted. Switched tools: a V8 CPU
|
|
667
|
+
profile (`node --cpu-prof`) — unlike span self-time, it sums to wall — showed a third of warm
|
|
668
|
+
`status` in `@teambit/objects`, half of whose parses were repeats caused by the 3,000-object LRU
|
|
669
|
+
cap. Bounding that cache by bytes instead ([#10723](https://github.com/teambit/bit/pull/10723)):
|
|
670
|
+
`status` **−15% wall, ~−170MB RSS**, `graph` −5%, others neutral (§4.5). First change in this
|
|
671
|
+
effort to move wall-time beyond noise. Phase 3 demoted; next candidates are the remaining §4.5
|
|
672
|
+
hotspots, each gated on its own A/B.
|
|
673
|
+
- 2026-09-24 — **#10723 merged** (`9307c9c6a`). Beyond the byte budget, it added a weak layer over the
|
|
674
|
+
objects LRU after simulation showed the budget alone regresses to master (slightly worse at 3×)
|
|
675
|
+
once a workspace outgrows it; with the weak layer, 3×/5× stay 17-24% faster and 26-33% leaner
|
|
676
|
+
than master (§4.5). Also recorded why RSS dropped: fewer throwaway copies from re-parsing
|
|
677
|
+
(8,702 → 4,715 parses), not retained duplicates.
|
|
678
|
+
- 2026-09-28 — **Component-cache cliff measured and removed** (§4.6). Simulating a larger workspace
|
|
679
|
+
(`BIT_CONFIG_CACHE_MAX_COMPONENTS`) took warm `status` from ~8.7s to 16-17s at a limit of 200 and
|
|
680
|
+
29-31s at 50. The three component caches thrash because `status` re-requests every component while
|
|
681
|
+
holding them all. [#10736](https://github.com/teambit/bit/pull/10736) adds a weak layer to them
|
|
682
|
+
(8.7s / 8.9s at 200 / 50, identical output). It exposed an env-ordering bug that eviction caused
|
|
683
|
+
and accidental reloads hid, fixed first in [#10735](https://github.com/teambit/bit/pull/10735).
|
|
684
|
+
- 2026-09-30 — **Profile follow-ups** (§4.7): [#10739](https://github.com/teambit/bit/pull/10739)
|
|
685
|
+
(−0.85s), [#10741](https://github.com/teambit/bit/pull/10741) (−0.85s, −115MB RSS),
|
|
686
|
+
[#10742](https://github.com/teambit/bit/pull/10742) (lookups 140 → 10ms). All merged. Next: the
|
|
687
|
+
head-to-tag scan (§4.7).
|
|
688
|
+
- 2026-10-01 — Benchmarked current master (§4): `status` 6.80s / 927MB, **−28% wall and −25% RSS**
|
|
689
|
+
since the September re-baseline. `graph` did not improve (19.63s, 2495MB) and is now the outlier.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Component Loading Redesign
|
|
2
2
|
|
|
3
|
-
**Status:** Phase 1 shipped; Phase 2
|
|
4
|
-
**Last updated:** 2026-
|
|
3
|
+
**Status:** Phase 1 shipped; Phase 2 re-scoped; Phase 3's premise disproved; object-cache fix shipped (see [Status](#status))
|
|
4
|
+
**Last updated:** 2026-09-24 (code references are against `master` @ `59855b104`, §4.5 @ `9307c9c6a`; line numbers will drift)
|
|
5
5
|
|
|
6
6
|
This document is the source of truth for a multi-phase effort to simplify Bit's component-loading
|
|
7
7
|
mechanism: fewer caches, a staged (lazy) loading pipeline, a single env/aspect load planner, and a
|
|
@@ -67,7 +67,9 @@ Problems:
|
|
|
67
67
|
|
|
68
68
|
- The workspace cache key embeds serialized `loadOpts`
|
|
69
69
|
(`createComponentCacheKey`, `workspace-component-loader.ts:990`), so the same component loaded
|
|
70
|
-
with different options is cached
|
|
70
|
+
with different options is cached under separate keys. _(Measured 2026-09-22: the keys point to
|
|
71
|
+
the same object, so this costs no memory — the real issue is that a key describes the request,
|
|
72
|
+
not what was loaded. See §4.3 item 2.)_
|
|
71
73
|
- Invalidation requires three coordinated calls (`workspace.ts:829-841`:
|
|
72
74
|
`componentLoader.clearCache` + legacy `clearComponentsCache` + `componentStatusLoader.clearCache`).
|
|
73
75
|
- Cache hits are silent — a stale cache is indistinguishable from a fresh load in the logs.
|
|
@@ -204,14 +206,30 @@ earlier ones teach us).
|
|
|
204
206
|
### Phase 2 — Quick perf wins on existing seams
|
|
205
207
|
|
|
206
208
|
- [x] Benchmark harness committed + baseline recorded (see §4) — **gate for the rest of the phase**
|
|
207
|
-
- [
|
|
209
|
+
- [x] ~~Forward `loadOpts` in the batch `executeLoadSlot` path~~ — **implemented, measured, rejected.**
|
|
210
|
+
Halved `on-load` aggregate self-time with **no wall change** and **+67MB RSS** (§4.3). Not merged.
|
|
211
|
+
- [ ] ~~Default `loadDocs: false, loadCompositions: false` for non-UI flows~~ — **blocked**: `status`
|
|
212
|
+
already sets them; they can't take effect until the cache stops keying on them (Phase 3)
|
|
213
|
+
- [ ] Lazy file contents in `ModelComponent.toConsumerComponent` (targets `graph`, and the RSS trend)
|
|
208
214
|
- [ ] `bit deps usage`: ids + stored deps instead of full load
|
|
209
215
|
- [ ] IDE metadata endpoint (`api-for-ide.ts`): S0-S2-level data only
|
|
210
216
|
- [ ] `bit remove` / forking: drop full-component loads where only ids/paths are used
|
|
211
|
-
|
|
217
|
+
|
|
218
|
+
**Explicitly dropped from this phase:** narrowing the deps-cache freshness fs scan (PR #10445,
|
|
219
|
+
closed 2026-09-22). It cut warm `status` fs syscalls 37% (74.3k → 46.4k) but moved wall ~0.3s —
|
|
220
|
+
the syscalls are `stat`/`lstat`/`readdir`, which overlap with CPU on a warm SSD — in exchange for
|
|
221
|
+
narrowing the invalidation signal of the highest-blast-radius cache in the loader. Bad trade. The
|
|
222
|
+
useful output of that work is the §4.1/§4.2 profiling and the node_modules invalidation constraints
|
|
223
|
+
recorded in the deps-cache notes.
|
|
212
224
|
|
|
213
225
|
### Phase 3 — Cache consolidation
|
|
214
226
|
|
|
227
|
+
> **Reassessed 2026-09-22 — not a perf lever.** Its promotion (§4.3) rested on the component cache
|
|
228
|
+
> holding duplicate copies; measured, it holds exactly one object per id (§4.3 item 2, §4.5). What
|
|
229
|
+
> remains is simplification and a correctness hazard (keys describe the request, not the content),
|
|
230
|
+
> worth doing only alongside partial-loading work that needs it. The 500-component cap on these
|
|
231
|
+
> caches is the one open perf question here (§4.5).
|
|
232
|
+
|
|
215
233
|
- [ ] Introduce `ComponentCacheManager` (unused, with tests)
|
|
216
234
|
- [ ] Migrate the workspace loader's four caches onto it
|
|
217
235
|
- [ ] Migrate scope loader + legacy ComponentLoader caches; single `invalidate(id, reason)` event
|
|
@@ -250,18 +268,52 @@ The `Peak RSS` column is the worst case across the four commands (`bit graph`).
|
|
|
250
268
|
median seconds. Per-command peak RSS for the baseline: status 1237MB, list 425MB, show 253MB,
|
|
251
269
|
graph 2013MB.
|
|
252
270
|
|
|
253
|
-
| Milestone
|
|
254
|
-
|
|
|
255
|
-
| Baseline (pre-Phase 2)
|
|
256
|
-
|
|
|
257
|
-
|
|
|
258
|
-
| After Phase
|
|
259
|
-
| After Phase
|
|
271
|
+
| Milestone | `bit status` | `bit list` | `bit show <comp>` | `bit graph --json` | Peak RSS |
|
|
272
|
+
| ------------------------ | ------------ | ---------- | ----------------- | ------------------ | -------- |
|
|
273
|
+
| Baseline (pre-Phase 2) | 11.24s | 1.59s | 1.73s | 20.49s | 2013MB |
|
|
274
|
+
| Re-baseline (2026-09-22) | 9.43s | 1.72s | 1.23s | 18.59s | 2352MB |
|
|
275
|
+
| 2026-10-01 (§4.5-4.7) | 6.80s | 1.57s | 1.19s | 19.63s | 2495MB |
|
|
276
|
+
| After Phase 2 | — | — | — | — | — |
|
|
277
|
+
| After Phase 3 | — | — | — | — | — |
|
|
278
|
+
| After Phase 4 | — | — | — | — | — |
|
|
279
|
+
| After Phase 5 | — | — | — | — | — |
|
|
260
280
|
|
|
261
281
|
Baseline measured 2026-06-15 on darwin (Apple silicon), `bit` @ 1.13.222, node v22.20.0,
|
|
262
282
|
`bit show teambit.workspace/workspace`. The two clear hotspots are `bit status` (11s) and
|
|
263
283
|
`bit graph` (20s, 2GB).
|
|
264
284
|
|
|
285
|
+
**Re-baseline 2026-09-22** — same machine/method, `bit` @ 2.2.57, node v22.20.0. Note the workspace
|
|
286
|
+
grew from ~313 to **331 components** (+5.8%), so per-component deltas are better than the raw wall
|
|
287
|
+
numbers suggest. Per-command peak RSS: status 1239MB, list 549MB, show 260MB, graph 2352MB.
|
|
288
|
+
|
|
289
|
+
| metric | baseline | re-baseline | raw Δ | per-component Δ |
|
|
290
|
+
| ----------------- | -------- | ----------- | ------ | --------------- |
|
|
291
|
+
| `bit status` wall | 11.24s | 9.43s | −16.1% | −20.6% |
|
|
292
|
+
| `bit graph` wall | 20.49s | 18.59s | −9.3% | −14.2% |
|
|
293
|
+
| `bit show` wall | 1.73s | 1.23s | −28.9% | n/a (1 comp) |
|
|
294
|
+
| `bit list` wall | 1.59s | 1.72s | +8.2% | +2.3% (flat) |
|
|
295
|
+
| `bit graph` RSS | 2013MB | 2352MB | +16.8% | +10.4% |
|
|
296
|
+
| `bit list` RSS | 425MB | 549MB | +29.2% | +22.2% |
|
|
297
|
+
|
|
298
|
+
Two things to read from this:
|
|
299
|
+
|
|
300
|
+
- **The wall-time gains came from outside this effort.** No Phase-2 item shipped, yet `status` and
|
|
301
|
+
`graph` improved materially — from targeted fixes landed independently (batching/deduping
|
|
302
|
+
dependency imports when building graphs from fs, dropping a per-invocation hard-link-directory
|
|
303
|
+
load, unifying component load paths). The redesign plan is not what moved these numbers.
|
|
304
|
+
- **Memory is trending the wrong way.** `graph` peak RSS is up 339MB and `list` up 124MB, beyond
|
|
305
|
+
what the +5.8% component growth explains. No phase currently targets memory; §2.1 lazy file
|
|
306
|
+
contents is the closest lever.
|
|
307
|
+
|
|
308
|
+
**2026-10-01**, after #10723 and §4.6-4.7, same machine/method, ~333 components. Per-command peak
|
|
309
|
+
RSS: status 927MB, list 546MB, show 249MB, graph 2495MB. Against the September re-baseline:
|
|
310
|
+
|
|
311
|
+
- `status` 9.43s → 6.80s (**−28%**) and 1239MB → 927MB (**−25%**).
|
|
312
|
+
- `list` −9%, `show` −3%.
|
|
313
|
+
- `graph` is **+6% wall and +6% RSS** (18.59s → 19.63s, 2352MB → 2495MB). The machine's load rose
|
|
314
|
+
during its runs (it went last), so some of this may be noise, but nothing here targeted it. `graph`
|
|
315
|
+
is now the outlier on both axes; its dominant cost is still file-content reads (§4.1).
|
|
316
|
+
|
|
265
317
|
### 4.1 Profiling findings (where the warm load time actually goes)
|
|
266
318
|
|
|
267
319
|
Beyond wall-time, an opt-in aggregate profiler (`BIT_LOAD_PROFILE=1 bit <cmd>`, in
|
|
@@ -303,18 +355,258 @@ Numbers below are aggregate self-time across ~313 components at ~6× concurrency
|
|
|
303
355
|
`deps usage`/IDE/`remove`/forking; `loadDocs/loadCompositions: false` trims `status`'s slot work
|
|
304
356
|
(~1.7s). The big `status` number is deferred to the staged-loading phase.
|
|
305
357
|
|
|
358
|
+
### 4.2 Re-profile 2026-09-22 (`bit status`, 331 components, 9.43s wall)
|
|
359
|
+
|
|
360
|
+
`BIT_LOAD_PROFILE=1 bit status`, aggregate self-time 68.1s (wall was 9.43s — the sum exceeds wall
|
|
361
|
+
because concurrently-interleaved spans each count their own `await` time).
|
|
362
|
+
|
|
363
|
+
> **Do not read these as wall-time budgets.** §4.3 shows, by direct experiment, that cutting a row
|
|
364
|
+
> here in half changed wall by nothing. Use this table to find _where to look_, never to size a win.
|
|
365
|
+
|
|
366
|
+
| stage | self-time | calls | share of self-time |
|
|
367
|
+
| -------------------------------------------- | --------- | ----- | ------------------ |
|
|
368
|
+
| `legacy-load-deps` | 34.3s | 331 | 50% |
|
|
369
|
+
| `on-load` (docs, compositions, schema, pkg…) | 13.7s | 2317 | 20% |
|
|
370
|
+
| `dependency-resolution` | 6.7s | 331 | 10% |
|
|
371
|
+
| `workspace.get` | 5.5s | 3349 | 8% |
|
|
372
|
+
| `execute-load-slot` (own) | 4.5s | 331 | 7% |
|
|
373
|
+
| `consumer-fs-load` | ~0s self | 7 | — |
|
|
374
|
+
|
|
375
|
+
Observations:
|
|
376
|
+
|
|
377
|
+
- `legacy-load-deps` is **50% of all load self-time** — still dependency-object materialization on
|
|
378
|
+
cache _hit_, not resolution. Per-component it improved (137ms → 104ms, −24%) but it did not change
|
|
379
|
+
shape. Same picture as June, three months apart.
|
|
380
|
+
- `on-load` is **20%** across **2317 calls** (331 components × ~7 handlers). `bit status` passes
|
|
381
|
+
`loadDocs:false`/`loadCompositions:false`, but they are **ineffective on the batch path**:
|
|
382
|
+
`workspace-component-loader.ts:479-480` calls `executeLoadSlot(component)` without forwarding
|
|
383
|
+
`loadOpts` (the single-component path at :1083/:1092 does forward them).
|
|
384
|
+
- `workspace.get` shows **3349 calls for 331 components (~10×)** — explained and tested in §4.4.
|
|
385
|
+
|
|
386
|
+
### 4.3 ⚠️ Aggregate self-time does not predict wall-time — validate before building
|
|
387
|
+
|
|
388
|
+
The `on-load` row above was tested directly, and the result invalidates the metric this section is
|
|
389
|
+
built on. Forwarding `loadOpts` on the batch path (the "one-line fix") was implemented and measured:
|
|
390
|
+
|
|
391
|
+
| metric | without fix | with fix | result |
|
|
392
|
+
| ------------------------------ | ----------- | ----------- | ---------------------- |
|
|
393
|
+
| `on-load` aggregate self-time | 13.7s | 6.6s | **−52%** |
|
|
394
|
+
| total load self-time | 68.1s | 53.5s | **−21%** |
|
|
395
|
+
| **`bit status` wall (median)** | **9.05s** | **8.98s** | **−0.07s — noise** |
|
|
396
|
+
| **peak RSS** | **~1228MB** | **~1295MB** | **+67MB — consistent** |
|
|
397
|
+
|
|
398
|
+
Wall is the median of 9 runs per variant, **interleaved** (A/B/A/B/A/B, recompiling between) to
|
|
399
|
+
cancel drift. Drift matters: the same unchanged code measured 9.43s and 8.99s twenty minutes apart,
|
|
400
|
+
a larger gap than the effect being tested. RSS is 4 runs per variant with zero overlap between the
|
|
401
|
+
two sets.
|
|
402
|
+
|
|
403
|
+
So halving the aggregate self-time of a stage worth "~1.9s of wall" by the divide-by-concurrency
|
|
404
|
+
estimate produced **no measurable wall change**, and cost memory. Two lessons:
|
|
405
|
+
|
|
406
|
+
1. **Aggregate self-time is a hypothesis generator, not a size estimate.** Spans are summed across
|
|
407
|
+
concurrently-interleaved async work on one JS thread, so most of what is counted is `await` time
|
|
408
|
+
overlapping other work, not critical-path CPU. Dividing by observed concurrency does **not**
|
|
409
|
+
recover wall-time. This is the third time the metric has misled this effort (the earlier "39s"
|
|
410
|
+
correction, the deps-cache fs-scan of #10445, and now this). **Do not size or prioritize work
|
|
411
|
+
from §4.1/§4.2 numbers — A/B the wall-clock first.**
|
|
412
|
+
2. ~~Forwarding `loadOpts` costs memory because the cache key embeds it.~~ **Retracted (§4.5).**
|
|
413
|
+
Instrumenting the cache showed every id does get two keys, but **both point to the same object**
|
|
414
|
+
(with or without the fix) — the partial-key entry is later mutated in place into the full
|
|
415
|
+
component. So there is no second copy; the +67MB has some other cause and is unexplained. What
|
|
416
|
+
_is_ true is subtler: the key records what the caller _asked for_, not what was loaded — `getMany`
|
|
417
|
+
stores every result under the full key (:203) regardless of its options and ignores
|
|
418
|
+
`storeInCache`. With the forwarding fix, docs-less components would have been served to later
|
|
419
|
+
full `get`s. That's a correctness hazard for partial loading, not a memory one.
|
|
420
|
+
|
|
421
|
+
**Method for the rest of this effort:** every perf claim lands with an interleaved wall-clock A/B on
|
|
422
|
+
`scripts/bench-component-loading.js` and a peak-RSS comparison. No merges justified by profiler
|
|
423
|
+
self-time alone.
|
|
424
|
+
|
|
425
|
+
### 4.4 The `workspace.get` 10× call count: explained, tested, and not worth a cache
|
|
426
|
+
|
|
427
|
+
Attributing every `workspace.get` call by caller (temporary stack-tally instrumentation, since
|
|
428
|
+
removed) on a warm `bit status`:
|
|
429
|
+
|
|
430
|
+
| caller | calls | note |
|
|
431
|
+
| ---------------------------------------------- | ----- | ------------------------------- |
|
|
432
|
+
| `envs` → `getEnvComponentByEnvId` (`opts={}`) | 2520 | resolving the **env** component |
|
|
433
|
+
| the batch load path (one per component) | 331 | expected |
|
|
434
|
+
| `component-dependency-factory` | 329 | ~one per component |
|
|
435
|
+
| `envs` (with `loadExtensions/executeLoadSlot`) | 162 | a second envs path |
|
|
436
|
+
| misc | 7 | |
|
|
437
|
+
|
|
438
|
+
**80% of all `workspace.get` calls are the envs aspect re-resolving the env component** — and this
|
|
439
|
+
workspace has **4 distinct envs**. `getEnvComponentByEnvId`
|
|
440
|
+
(`scopes/envs/envs/environments.main.runtime.ts`) does `host.get(envId)` on every call, and its
|
|
441
|
+
callers (dependency-resolver, dev-files, preview, and the env-descriptor path) each run per
|
|
442
|
+
component.
|
|
443
|
+
|
|
444
|
+
Memoizing it by `envId` was implemented and measured:
|
|
445
|
+
|
|
446
|
+
| metric | without memo | with memo | result |
|
|
447
|
+
| ----------------------------- | ------------ | --------- | -------------------------- |
|
|
448
|
+
| `workspace.get` calls | 3349 | 835 | **−75%** |
|
|
449
|
+
| `workspace.get` self-time | 5.48s | 5.09s | −0.39s — **nearly free** |
|
|
450
|
+
| `bit status` wall (median, 9) | 9.59s | 9.45s | −0.14s (−1.5%), consistent |
|
|
451
|
+
| peak RSS | ~1234MB | ~1246MB | +12MB, within noise |
|
|
452
|
+
|
|
453
|
+
**Conclusion: real but small, and the wrong fix.** Removing 75% of the calls moved
|
|
454
|
+
`workspace.get` self-time by only 0.39s, which proves those 2514 calls were already cheap cache
|
|
455
|
+
hits — the alarming 10× count was a **third red herring**, consistent with §4.3. The 1.5% wall gain
|
|
456
|
+
is genuine but would be bought by adding a 12th ad-hoc cache, with its own invalidation surface, to
|
|
457
|
+
a system whose stated core problem (§1.2) is _~11 uncoordinated caches_. That is the anti-pattern
|
|
458
|
+
this effort exists to remove.
|
|
459
|
+
|
|
460
|
+
**Where it belongs instead:** this is direct evidence for the standalone **`EnvResolver`** of §2.4 —
|
|
461
|
+
env identity is asked for constantly and should be resolvable from S0-S2 data with one owner and one
|
|
462
|
+
cache, rather than by loading a full env Component through the general loader 2682 times. Fold it
|
|
463
|
+
into Phase 5, and do not bolt on a point-memo before then.
|
|
464
|
+
|
|
465
|
+
### 4.5 A real CPU profile: a third of `status` is the object layer, and it was thrashing
|
|
466
|
+
|
|
467
|
+
The span profiler (§4.1-4.4) misled three times, so `bit status` was profiled with V8 instead
|
|
468
|
+
(`node --cpu-prof`). V8 samples measure on-thread time, which _does_ sum to wall-time (9.17s
|
|
469
|
+
sampled: 7.7s JS/native, 0.83s GC, 0.46s idle). **Use this, not `BIT_LOAD_PROFILE`, to decide
|
|
470
|
+
where to work.**
|
|
471
|
+
|
|
472
|
+
The dominant cost is not in the component loader at all: **`@teambit/objects` is 1.7s self / 3.1s
|
|
473
|
+
inclusive** — about a third of wall. Mostly `parseObject` (1.1s, Version/ModelComponent/Source
|
|
474
|
+
JSON parsing). Counting parses per hash showed **half of them were repeats**: status touches ~4,700
|
|
475
|
+
distinct objects, and the `Repository` LRU was capped at **3,000 objects**, so it evicted and
|
|
476
|
+
re-parsed ~4,000. Caching everything costs ~124MB inflated (22MB on disk).
|
|
477
|
+
|
|
478
|
+
The count cap had been lowered twice for OOM reasons (10K → 5K → 3K), and objects range from bytes
|
|
479
|
+
to ~1.7MB, so raising the count would repeat a known mistake. Fixed instead by **bounding the cache
|
|
480
|
+
by approximate inflated bytes** (256MB default) — [#10723](https://github.com/teambit/bit/pull/10723) (merged):
|
|
481
|
+
|
|
482
|
+
| interleaved A/B | old (3,000 objects) | byte budget | result |
|
|
483
|
+
| --------------------------------- | ------------------- | ----------- | --------------------- |
|
|
484
|
+
| `bit status` wall (median, 8) | 8.64s | 7.37s | **−1.27s (−15%)** |
|
|
485
|
+
| `bit status` wall range | 8.50-8.91s | 7.36-7.48s | no overlap, 8/8 pairs |
|
|
486
|
+
| `bit status` peak RSS | ~1305MB | ~1130MB | **~−170MB** |
|
|
487
|
+
| `bit graph --json` wall (3 pairs) | 20.91s | 19.94s | −0.97s (−5%) |
|
|
488
|
+
| `bit list` / `bit show` | — | — | neutral |
|
|
489
|
+
|
|
490
|
+
It wins on memory too, but not because the old cache retained duplicates: only ~78 duplicate copies
|
|
491
|
+
(2.6MB) were live at the end of the old run. The win is **churn**: each of the ~4,000 re-parses
|
|
492
|
+
inflated and parsed a fresh copy that became garbage, so the old policy did 8,702 parses vs 4,715.
|
|
493
|
+
Peak `arrayBuffers` fell ~214→~103MB, peak `heapUsed` ~840→~768MB, and GC time ~820→~635ms. This is
|
|
494
|
+
the first change in this effort that moved wall-time by more than noise — and none of the phases
|
|
495
|
+
pointed at it.
|
|
496
|
+
|
|
497
|
+
**The cache is essential, but a byte budget alone has a cliff.** With the cache disabled (`max 1`),
|
|
498
|
+
`status` takes 22.4s with 31,213 parses. A workspace whose working set exceeds the budget falls back
|
|
499
|
+
to master's thrashing, and was slightly _worse_ than master at 3×. So #10723 also added a **weak layer** (`LiveObjects`, `objects/live-objects.ts`): a
|
|
500
|
+
hash → `WeakRef` map with a `FinalizationRegistry`. On an LRU miss, it returns the object if
|
|
501
|
+
something else (e.g. a loaded component) still references it; it never keeps anything alive by
|
|
502
|
+
itself. Objects too big for the LRU (>100KB compressed) are tracked there too. Alone it's
|
|
503
|
+
insufficient (9.5s vs 7.7s: most objects are unreferenced between uses); on top of the LRU it's a no-op while
|
|
504
|
+
the budget fits and removes the cliff when it doesn't. Simulated larger workspaces (both limits
|
|
505
|
+
shrunk by the same factor, `status`, median of 2, builds alternated):
|
|
506
|
+
|
|
507
|
+
| simulated size | master | byte budget | byte budget + weak |
|
|
508
|
+
| -------------- | -------------- | -------------- | ------------------ |
|
|
509
|
+
| this repo | 8.9s / 1317MB | 7.7s / 1140MB | 7.8s / 1110MB |
|
|
510
|
+
| 3× | 10.5s / 1629MB | 11.2s / 1683MB | **8.7s / 1200MB** |
|
|
511
|
+
| 5× | 12.1s / 1934MB | 12.1s / 1874MB | **9.2s / 1303MB** |
|
|
512
|
+
|
|
513
|
+
Correctness notes for anyone touching it: a `ModelComponent`'s hash is name-based, so one hash maps
|
|
514
|
+
to different content over time — every path that writes, removes, or clears the LRU must update the
|
|
515
|
+
weak map too (`_writeOne` only after the write succeeds; `removeFromCache`; `clearObjectsFromCache`).
|
|
516
|
+
|
|
517
|
+
Other hotspots from the same profile, since acted on (§4.6, §4.7):
|
|
518
|
+
|
|
519
|
+
- `ModelComponent.versionsIncludeOrphaned`, a getter that spreads `{...versions, ...orphanedVersions}`
|
|
520
|
+
on **every access** (~375ms) → single-tag lookups no longer use it ([#10739](https://github.com/teambit/bit/pull/10739)).
|
|
521
|
+
- `getTagOfRefIfExists`, a linear scan over all tags (~150ms self) → no longer copies; still linear (§4.7).
|
|
522
|
+
- `Version.calculateHash` from `isComponentModified` (~285ms) → [#10741](https://github.com/teambit/bit/pull/10741).
|
|
523
|
+
- `status` parses ~3,300 `Source` objects (file contents from the model). The "lazy file contents"
|
|
524
|
+
item of Phase 2 would remove these parses entirely. Still open.
|
|
525
|
+
- The **component** caches are capped at **500**, and this workspace has 339 → measured in §4.6: the
|
|
526
|
+
cliff was real, and steep.
|
|
527
|
+
|
|
528
|
+
### 4.6 The component-cache cliff: eviction freed nothing and forced rebuilds
|
|
529
|
+
|
|
530
|
+
A larger workspace was simulated by shrinking the component caches' limit
|
|
531
|
+
(`BIT_CONFIG_CACHE_MAX_COMPONENTS=N`, which bounds the 8 caches sized by `cache.max.components`).
|
|
532
|
+
On master, warm `bit status`:
|
|
533
|
+
|
|
534
|
+
| limit | wall | peak RSS |
|
|
535
|
+
| ------------- | ---------- | -------- |
|
|
536
|
+
| default (500) | ~8.7s | ~1.1GB |
|
|
537
|
+
| 200 | 16.1-17.0s | 1.47GB |
|
|
538
|
+
| 50 | 29-30.8s | 1.65GB |
|
|
539
|
+
|
|
540
|
+
So a workspace a few times this size would take 2-3× longer. Isolating each cache (all but one
|
|
541
|
+
unbounded) put the cliff on the three **component** caches: the workspace loader's, the scope
|
|
542
|
+
loader's, and the legacy `ComponentLoader`'s.
|
|
543
|
+
|
|
544
|
+
The cause is a scan pattern that an LRU can't serve. `status` requests every component several times
|
|
545
|
+
over, and between passes it keeps all of them alive in its own list. Evicting a component therefore
|
|
546
|
+
frees no memory (the status list still holds it); it only makes the next request rebuild it from
|
|
547
|
+
scratch. Hence higher RSS at lower limits, not lower.
|
|
548
|
+
|
|
549
|
+
Fixed with the same idea as §4.5, applied to the component caches:
|
|
550
|
+
[#10736](https://github.com/teambit/bit/pull/10736) adds a `weak: true` option to `createInMemoryCache`
|
|
551
|
+
(`WeakValues`: a `WeakRef` map beside the LRU, cleaned by a `FinalizationRegistry`). On a miss it
|
|
552
|
+
returns the evicted value if it is still alive and puts it back in the LRU. `keys()`, `delete` and
|
|
553
|
+
`deleteAll` include the weak entries, so key-based invalidation (`clearComponentsCache` iterates
|
|
554
|
+
`keys()`) still clears them. It never keeps anything alive on its own.
|
|
555
|
+
|
|
556
|
+
| limit | master | with #10735 + #10736 |
|
|
557
|
+
| ----- | ------------------- | -------------------- |
|
|
558
|
+
| 200 | 16.1-17.0s / 1.47GB | 8.7s / 1.1GB |
|
|
559
|
+
| 50 | 29-30.8s / 1.65GB | 8.9s / 1.1GB |
|
|
560
|
+
|
|
561
|
+
At limit 50 the output is identical to the default.
|
|
562
|
+
|
|
563
|
+
**The weak layer exposed a latent bug, fixed separately in [#10735](https://github.com/teambit/bit/pull/10735).**
|
|
564
|
+
With it on the legacy cache, `status` reported 23 "failed loading env" issues. `buildLoadGroups`
|
|
565
|
+
passed each component's extension data to the next step **through** the bounded
|
|
566
|
+
`componentsExtensionsCache`. Entries evicted in between were missing, so their envs were left out of
|
|
567
|
+
the env-first load group, and components were loaded before their env was registered. On master this
|
|
568
|
+
was hidden by accidental reloads later in the command, the same rebuilds that made it slow.
|
|
569
|
+
`populateScopeAndExtensionsCache` now returns the data as a map, and the cache is only a cache again.
|
|
570
|
+
Performance-neutral on its own.
|
|
571
|
+
|
|
572
|
+
### 4.7 Follow-ups from the CPU profile: copies nobody reads
|
|
573
|
+
|
|
574
|
+
With the caches fixed, the profile was flat enough that the next items were plain wasted work:
|
|
575
|
+
|
|
576
|
+
| PR | change | measured |
|
|
577
|
+
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------- |
|
|
578
|
+
| [#10739](https://github.com/teambit/bit/pull/10739) | single-tag lookups (`getRef`, `getTagOfRefIfExists`, `sources.get/exists`) read `versions`/`orphanedVersions` instead of copying both | `status` 7.6 → 6.8s (5 interleaved rounds) |
|
|
579
|
+
| [#10741](https://github.com/teambit/bit/pull/10741) | `Version.id()` (the hash) builds only its 8 fields, not all of `toObject()`; `toConfigArray` clones only the config; ids not cloned in `toConsumerComponent` | `status` 8.5 → 7.65s, peak RSS 1.1GB → 985MB (5 rounds) |
|
|
580
|
+
| [#10742](https://github.com/teambit/bit/pull/10742) | `.bitmap` lookups index ids by name; the `IfExist` variants no longer throw and catch | time in those lookups ~140ms → ~10ms (CPU profile) |
|
|
581
|
+
|
|
582
|
+
`Version.id()` is a content hash, so its output must not change: the old and new implementations were
|
|
583
|
+
compared on all 15,398 version objects in this repo's scope, with identical results. Output of
|
|
584
|
+
`bit status` was identical in every A/B.
|
|
585
|
+
|
|
586
|
+
Checked and not worth acting on:
|
|
587
|
+
|
|
588
|
+
- **Object parsing (~590ms).** After #10723, each of the ~4,700 objects `status` touches is parsed
|
|
589
|
+
once (15 repeats, all `Source`). Real work, no longer churn.
|
|
590
|
+
- **Module loading.** `bin/bit.js` already enables Node's compile cache; the cost only shows up
|
|
591
|
+
right after a recompile.
|
|
592
|
+
|
|
593
|
+
Still open: `getHeadRegardlessOfLaneAsTagOrHash` scans all tags to find the head's tag (~145ms,
|
|
594
|
+
several callers per component). The head is usually the newest tag, so searching from the end would
|
|
595
|
+
find it at once. `extensions.clone()` in `toConsumerComponent` (~175ms) is needed, since it keeps a
|
|
596
|
+
loaded component from mutating the cached `Version`.
|
|
597
|
+
|
|
306
598
|
---
|
|
307
599
|
|
|
308
600
|
## Status
|
|
309
601
|
|
|
310
|
-
| Phase | State
|
|
311
|
-
| ----------------------- |
|
|
312
|
-
| 1 — Observability | done
|
|
313
|
-
| 2 — Quick perf wins |
|
|
314
|
-
| 3 — Cache consolidation | not started | — | —
|
|
315
|
-
| 4 — Staged pipeline | not started
|
|
316
|
-
| 5 — Env planner | not started
|
|
317
|
-
| 6 — Legacy inversion | not started
|
|
602
|
+
| Phase | State | OpenSpec change | PRs |
|
|
603
|
+
| ----------------------- | ----------------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
604
|
+
| 1 — Observability | done | `component-load-observability` | [#10418](https://github.com/teambit/bit/pull/10418) |
|
|
605
|
+
| 2 — Quick perf wins | re-scoped, 1/7 done; profile-driven fixes merged (§4.5-4.7) | — | [#10445](https://github.com/teambit/bit/pull/10445) (closed, not merged), [#10723](https://github.com/teambit/bit/pull/10723), [#10735](https://github.com/teambit/bit/pull/10735), [#10736](https://github.com/teambit/bit/pull/10736), [#10739](https://github.com/teambit/bit/pull/10739), [#10741](https://github.com/teambit/bit/pull/10741), [#10742](https://github.com/teambit/bit/pull/10742) |
|
|
606
|
+
| 3 — Cache consolidation | not started — premise disproved, demoted (§4.5) | — | — |
|
|
607
|
+
| 4 — Staged pipeline | not started | — | — |
|
|
608
|
+
| 5 — Env planner | not started | — | — |
|
|
609
|
+
| 6 — Legacy inversion | not started | — | — |
|
|
318
610
|
|
|
319
611
|
**Log:**
|
|
320
612
|
|
|
@@ -340,3 +632,58 @@ Numbers below are aggregate self-time across ~313 components at ~6× concurrency
|
|
|
340
632
|
file-content reads (`consumer-fs-load`, 5.8s) → the target for lazy file contents. (Correction: an
|
|
341
633
|
earlier "39s" figure was aggregate-concurrent self-time, not wall; warm wall is ~13s.) Direction
|
|
342
634
|
for the next Phase-2 PR intentionally left open.
|
|
635
|
+
- 2026-09-22 — **Phase 2 reassessed after a 3-month gap.** Re-baselined (§4) and re-profiled (§4.2).
|
|
636
|
+
Findings:
|
|
637
|
+
1. `status` −16% / `graph` −9% / `show` −29% since June, **with no Phase-2 item shipped** — the
|
|
638
|
+
gains came from targeted fixes landed outside this effort. Peak RSS regressed (`graph` +339MB,
|
|
639
|
+
`list` +124MB); nothing currently targets memory.
|
|
640
|
+
2. The structural conclusion is confirmed a second time: `legacy-load-deps` is now **50% of all
|
|
641
|
+
load self-time**, still materialization-on-cache-hit. Phase 4 is the lever; Phase 2 was never
|
|
642
|
+
going to move it. Phase 4 promoted to the next high-value target.
|
|
643
|
+
3. Found the concrete cheap win: `bit status` already sets `loadDocs:false`/`loadCompositions:false`
|
|
644
|
+
but the batch path drops them (`workspace-component-loader.ts:479-480` doesn't forward
|
|
645
|
+
`loadOpts`), leaving ~1.9s of `on-load` work on the table across 2317 handler calls.
|
|
646
|
+
4. New unexplained signal: `workspace.get` is called **3349× for 331 components (~10×)**.
|
|
647
|
+
5. **The cheap win was implemented and rejected on measurement** (§4.3). Forwarding `loadOpts`
|
|
648
|
+
halved `on-load` self-time but moved wall by −0.07s (noise) and cost +67MB RSS, because the
|
|
649
|
+
component cache keys on `loadDocs`/`loadCompositions` and so stores a second copy. This
|
|
650
|
+
invalidated the prioritization metric: **aggregate self-time does not predict wall-time**, for
|
|
651
|
+
the third time in this effort. Phase 4 is therefore _not_ promoted on the strength of the
|
|
652
|
+
`legacy-load-deps` 50% figure; **Phase 3 (cache consolidation) is promoted instead**, since the
|
|
653
|
+
loadOpts-in-cache-key problem blocks every partial-loading idea in Phases 2 and 4. All further
|
|
654
|
+
perf claims require an interleaved wall-clock A/B plus peak-RSS comparison.
|
|
655
|
+
6. **The `workspace.get` 10× count was chased down and also rejected** (§4.4). 80% of the 3349
|
|
656
|
+
calls are the envs aspect re-resolving one of only 4 distinct envs. Memoizing removes 75% of
|
|
657
|
+
the calls for −0.14s (−1.5%) wall — real, but it buys that by adding a 12th ad-hoc cache to the
|
|
658
|
+
~11 the effort exists to consolidate. Recorded as evidence for the §2.4 `EnvResolver` (Phase 5)
|
|
659
|
+
instead. Net result of this session: three candidate quick wins investigated, **all three
|
|
660
|
+
rejected on measurement**, and the prioritization metric itself invalidated.
|
|
661
|
+
PR #10445 closed unmerged (see §3 for the rationale). The abandoned May branch
|
|
662
|
+
`refactor/component-loading-v2-take-3` (`UnifiedComponentLoader` behind `BIT_LOADER=new`) predates
|
|
663
|
+
this document and is superseded by the phase plan — not to be resumed.
|
|
664
|
+
- 2026-09-22 — **Phase 3 started, and its premise disproved in the first hour.** Instrumenting the
|
|
665
|
+
component cache found one object per id (two keys, same object), so consolidating it saves no
|
|
666
|
+
memory; §4.3's explanation of the +67MB was wrong and is retracted. Switched tools: a V8 CPU
|
|
667
|
+
profile (`node --cpu-prof`) — unlike span self-time, it sums to wall — showed a third of warm
|
|
668
|
+
`status` in `@teambit/objects`, half of whose parses were repeats caused by the 3,000-object LRU
|
|
669
|
+
cap. Bounding that cache by bytes instead ([#10723](https://github.com/teambit/bit/pull/10723)):
|
|
670
|
+
`status` **−15% wall, ~−170MB RSS**, `graph` −5%, others neutral (§4.5). First change in this
|
|
671
|
+
effort to move wall-time beyond noise. Phase 3 demoted; next candidates are the remaining §4.5
|
|
672
|
+
hotspots, each gated on its own A/B.
|
|
673
|
+
- 2026-09-24 — **#10723 merged** (`9307c9c6a`). Beyond the byte budget, it added a weak layer over the
|
|
674
|
+
objects LRU after simulation showed the budget alone regresses to master (slightly worse at 3×)
|
|
675
|
+
once a workspace outgrows it; with the weak layer, 3×/5× stay 17-24% faster and 26-33% leaner
|
|
676
|
+
than master (§4.5). Also recorded why RSS dropped: fewer throwaway copies from re-parsing
|
|
677
|
+
(8,702 → 4,715 parses), not retained duplicates.
|
|
678
|
+
- 2026-09-28 — **Component-cache cliff measured and removed** (§4.6). Simulating a larger workspace
|
|
679
|
+
(`BIT_CONFIG_CACHE_MAX_COMPONENTS`) took warm `status` from ~8.7s to 16-17s at a limit of 200 and
|
|
680
|
+
29-31s at 50. The three component caches thrash because `status` re-requests every component while
|
|
681
|
+
holding them all. [#10736](https://github.com/teambit/bit/pull/10736) adds a weak layer to them
|
|
682
|
+
(8.7s / 8.9s at 200 / 50, identical output). It exposed an env-ordering bug that eviction caused
|
|
683
|
+
and accidental reloads hid, fixed first in [#10735](https://github.com/teambit/bit/pull/10735).
|
|
684
|
+
- 2026-09-30 — **Profile follow-ups** (§4.7): [#10739](https://github.com/teambit/bit/pull/10739)
|
|
685
|
+
(−0.85s), [#10741](https://github.com/teambit/bit/pull/10741) (−0.85s, −115MB RSS),
|
|
686
|
+
[#10742](https://github.com/teambit/bit/pull/10742) (lookups 140 → 10ms). All merged. Next: the
|
|
687
|
+
head-to-tag scan (§4.7).
|
|
688
|
+
- 2026-10-01 — Benchmarked current master (§4): `status` 6.80s / 927MB, **−28% wall and −25% RSS**
|
|
689
|
+
since the September re-baseline. `graph` did not improve (19.63s, 2495MB) and is now the outlier.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import * as compositions_0 from '/home/circleci/Library/Caches/Bit/capsules/8891be5ad/teambit.workspace_workspace@1.0.
|
|
2
|
-
import * as overview_0 from '/home/circleci/Library/Caches/Bit/capsules/8891be5ad/teambit.workspace_workspace@1.0.
|
|
1
|
+
import * as compositions_0 from '/home/circleci/Library/Caches/Bit/capsules/8891be5ad/teambit.workspace_workspace@1.0.1194/dist/workspace.composition.js';
|
|
2
|
+
import * as overview_0 from '/home/circleci/Library/Caches/Bit/capsules/8891be5ad/teambit.workspace_workspace@1.0.1194/dist/workspace.docs.mdx';
|
|
3
3
|
|
|
4
4
|
export const compositions = [compositions_0];
|
|
5
5
|
export const overview = [overview_0];
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teambit/workspace",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1194",
|
|
4
4
|
"homepage": "https://bit.cloud/teambit/workspace/workspace",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"componentId": {
|
|
7
7
|
"scope": "teambit.workspace",
|
|
8
8
|
"name": "workspace",
|
|
9
|
-
"version": "1.0.
|
|
9
|
+
"version": "1.0.1194"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"lodash": "4.17.21",
|
|
@@ -30,26 +30,42 @@
|
|
|
30
30
|
"reset-css": "5.0.1",
|
|
31
31
|
"@teambit/component-id": "1.2.4",
|
|
32
32
|
"@teambit/harmony": "0.4.12",
|
|
33
|
+
"@teambit/legacy.extension-data": "0.0.175",
|
|
34
|
+
"@teambit/legacy.scope": "0.0.173",
|
|
33
35
|
"@teambit/component-version": "1.0.4",
|
|
36
|
+
"@teambit/legacy.consumer-component": "0.0.174",
|
|
37
|
+
"@teambit/legacy.consumer": "0.0.173",
|
|
34
38
|
"@teambit/bit-error": "0.0.404",
|
|
35
39
|
"@teambit/lane-id": "0.0.312",
|
|
40
|
+
"@teambit/legacy.bit-map": "0.0.230",
|
|
36
41
|
"@teambit/toolbox.fs.last-modified": "0.0.21",
|
|
37
42
|
"@teambit/toolbox.path.path": "0.0.23",
|
|
38
43
|
"@teambit/graph.cleargraph": "0.0.11",
|
|
39
44
|
"@teambit/logger": "0.0.1494",
|
|
45
|
+
"@teambit/cli": "0.0.1404",
|
|
46
|
+
"@teambit/config-store": "0.0.285",
|
|
40
47
|
"@teambit/legacy.constants": "0.0.46",
|
|
41
48
|
"@teambit/component.ui.component-status-resolver": "0.0.510",
|
|
42
49
|
"@teambit/harmony.modules.load-trace": "0.0.15",
|
|
43
50
|
"@teambit/harmony.modules.resolved-component": "0.0.528",
|
|
44
51
|
"@teambit/legacy.utils": "0.0.52",
|
|
52
|
+
"@teambit/scope.remotes": "0.0.173",
|
|
53
|
+
"@teambit/config": "0.0.1579",
|
|
45
54
|
"@teambit/harmony.modules.requireable-component": "0.0.528",
|
|
46
55
|
"@teambit/harmony.modules.runtimes": "0.0.1",
|
|
47
56
|
"@teambit/toolbox.modules.module-resolver": "0.0.27",
|
|
57
|
+
"@teambit/workspace.modules.node-modules-linker": "0.0.406",
|
|
58
|
+
"@teambit/global-config": "0.0.1408",
|
|
59
|
+
"@teambit/legacy.consumer-config": "0.0.173",
|
|
60
|
+
"@teambit/variants": "0.0.1672",
|
|
48
61
|
"@teambit/component-issues": "0.0.188",
|
|
62
|
+
"@teambit/component.sources": "0.0.225",
|
|
49
63
|
"@teambit/dependencies.modules.packages-excluder": "1.0.8",
|
|
50
64
|
"@teambit/git.modules.git-executable": "0.0.47",
|
|
51
65
|
"@teambit/harmony.modules.in-memory-cache": "0.0.51",
|
|
52
66
|
"@teambit/legacy-bit-id": "1.1.3",
|
|
67
|
+
"@teambit/legacy.component-list": "0.0.227",
|
|
68
|
+
"@teambit/legacy.scope-api": "0.0.228",
|
|
53
69
|
"@teambit/toolbox.path.is-path-inside": "0.0.516",
|
|
54
70
|
"@teambit/workspace.modules.match-pattern": "0.0.536",
|
|
55
71
|
"@teambit/component.ui.component-drawer": "0.0.510",
|
|
@@ -86,39 +102,23 @@
|
|
|
86
102
|
"@teambit/workspace.hooks.use-url-change-broadcaster": "0.0.6",
|
|
87
103
|
"@teambit/workspace.ui.preserve-workspace-mode": "0.0.7",
|
|
88
104
|
"@teambit/evangelist.elements.icon": "1.0.5",
|
|
89
|
-
"@teambit/component": "1.0.
|
|
90
|
-
"@teambit/dependency-resolver": "1.0.
|
|
91
|
-
"@teambit/envs": "1.0.
|
|
92
|
-
"@teambit/
|
|
93
|
-
"@teambit/
|
|
94
|
-
"@teambit/
|
|
95
|
-
"@teambit/
|
|
96
|
-
"@teambit/
|
|
97
|
-
"@teambit/
|
|
98
|
-
"@teambit/
|
|
99
|
-
"@teambit/
|
|
100
|
-
"@teambit/
|
|
101
|
-
"@teambit/
|
|
102
|
-
"@teambit/
|
|
103
|
-
"@teambit/
|
|
104
|
-
"@teambit/
|
|
105
|
-
"@teambit/
|
|
106
|
-
"@teambit/aspect-loader": "1.0.1192",
|
|
107
|
-
"@teambit/config": "0.0.1579",
|
|
108
|
-
"@teambit/workspace.modules.node-modules-linker": "0.0.406",
|
|
109
|
-
"@teambit/graphql": "1.0.1192",
|
|
110
|
-
"@teambit/bundler": "1.0.1192",
|
|
111
|
-
"@teambit/global-config": "0.0.1408",
|
|
112
|
-
"@teambit/legacy.consumer-config": "0.0.173",
|
|
113
|
-
"@teambit/ui": "1.0.1192",
|
|
114
|
-
"@teambit/variants": "0.0.1672",
|
|
115
|
-
"@teambit/component.sources": "0.0.225",
|
|
116
|
-
"@teambit/legacy.component-list": "0.0.227",
|
|
117
|
-
"@teambit/legacy.scope-api": "0.0.228",
|
|
118
|
-
"@teambit/command-bar": "1.0.1192",
|
|
119
|
-
"@teambit/sidebar": "1.0.1192",
|
|
120
|
-
"@teambit/pubsub": "1.0.1192",
|
|
121
|
-
"@teambit/deprecation": "1.0.1192"
|
|
105
|
+
"@teambit/component": "1.0.1194",
|
|
106
|
+
"@teambit/dependency-resolver": "1.0.1194",
|
|
107
|
+
"@teambit/envs": "1.0.1194",
|
|
108
|
+
"@teambit/objects": "0.0.701",
|
|
109
|
+
"@teambit/scope": "1.0.1194",
|
|
110
|
+
"@teambit/graph": "1.0.1194",
|
|
111
|
+
"@teambit/isolator": "1.0.1194",
|
|
112
|
+
"@teambit/component-tree": "1.0.1194",
|
|
113
|
+
"@teambit/watcher": "1.0.1194",
|
|
114
|
+
"@teambit/aspect-loader": "1.0.1194",
|
|
115
|
+
"@teambit/graphql": "1.0.1194",
|
|
116
|
+
"@teambit/bundler": "1.0.1194",
|
|
117
|
+
"@teambit/ui": "1.0.1194",
|
|
118
|
+
"@teambit/command-bar": "1.0.1194",
|
|
119
|
+
"@teambit/sidebar": "1.0.1194",
|
|
120
|
+
"@teambit/pubsub": "1.0.1194",
|
|
121
|
+
"@teambit/deprecation": "1.0.1194"
|
|
122
122
|
},
|
|
123
123
|
"devDependencies": {
|
|
124
124
|
"@types/lodash": "4.14.165",
|