Skip to content

Every department worker builds the package catalog independently at start, and 29 doing it at once makes each 13x slower #416

Description

@macstudio-4

What happens

At supervisor start, one worker child is forked per department, all within about a second. Each then performs the same full package-catalog initialization independently. Done alone that costs ~11 s; done by 29 workers at once it costs ~141 s each. Until a worker finishes, the cron tick already sitting in its queue cannot be served, so the first useful work of a deployment happens about two and a half minutes after start.

Measured

Fork time comes from the child log filename's spawn epoch; first-work time from that child's own EVENT=code_provenance line. 448 children across three deployments, one run:

  spawned within 30 s of supervisor start (29 children):  median 141 s   max 143 s
  spawned more than 10 min after start   (263 children):  median  11 s   max  89 s

The same initialization, 13x slower when it is done concurrently.

The burst itself, from the packages supervisor log — MSG=framework spawned, 19 distinct pids in one second:

  15:52:25   1
  15:52:26  19      <-- one per department
  15:52:27   2
  15:52:32   1
  ...

Independently confirmed by the child log filenames: 19 files carry spawn epoch 1787673146.

One of those 19, end to end:

15:52:26  forked           (supervisor: framework spawned, pid 20501)
15:54:47  code_provenance  first line the child emits, 141 s later
15:54:52  ENTRY            queue=github-devloop-ops.devloop_observe_tick

Where the time goes

sample on a child in that state, 4322 samples, single thread, one path, every sample:

main → run → parse_args → PackageRoots::resolve_run → from_canonical
  → external_package_catalogs → UnitCatalog::discover → from_workspace_inner
  → build_indexes → build_own_module_indexes → scan_own_modules
  → scan_lua_modules → scan_lua_modules_inner
  → Path::canonicalize → realpath → __getattrlist        3905/4322 = 90%

Each child is handed --project-root plus 15 --package-root arguments; those roots hold 2140 .lua files between them, and scan_lua_modules_inner calls canonicalize once per file:

let logical = logical_module_name(root, &path, allow_root_init)?;
insert_path_entry(
    modules,
    logical,
    path.canonicalize()
        .with_context(|| format!("canonicalize {}", path.display()))?,
    &format!("module root {}", root.display()),
)?;

realpath resolves every component of every path, so the shared directory prefixes are re-resolved once per file, and then that whole effort is repeated in full by each of the other 18 workers.

The jitter is on the wrong thing

spawn_cron already staggers first fires deterministically:

let mut next = Instant::now() + cron_first_fire_jitter(&name, interval);

Recomputing that function over this deployment's 17 raisers (all intervals >= 300 s, so bound is the full 30 s) gives fires spread from 0.20 s to 28.41 s, at most 3 landing in any one second. That is working as designed.

But the ticks are not what stampedes. The workers are forked together regardless, and a jittered tick that arrives at +1.93 s waits behind a worker that will not be ready until +141 s. Spreading the ticks cannot help while the workers all initialize at once.

Two earlier reports on this, both wrong

I filed #413 claiming the scan was the cost because a sampled process was in it, and #415 claiming departments were killed by a stall watchdog because I read a delivery-age field as process lifetime. Both are closed, both were wrong on mechanism. #413 was closer than I gave it credit for when I retracted it: the retraction rested on a proxy scan of one root (1001 files) finishing in ~1.5 s, which understated the real scope (16 roots, 2140 files) and, more importantly, measured no startup stampede at all.

What is different here is that the claim is now a measured gap between two populations of the same system — 141 s against 11 s — rather than an inference from one stack.

What would close it

Either side would do, and they are independent:

  • The per-file canonicalize is the inner cost and repeats prefix resolution 2140 times per worker.
  • 19 workers computing an identical catalog from identical inputs at the same moment is the multiplier. Nothing in the inputs differs between them.

The property to hold: bringing a deployment up should not cost every worker a full independent catalog build at the same instant, and a deployment's first tick should not wait minutes behind worker initialization.

No activity

Activity on this issue will appear here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions