Skip to content

Meta: Stop building vcpkg dependencies serially - #10963

Open
alimpfard wants to merge 1 commit into
LadybirdBrowser:masterfrom
alimpfard:vcpkg-parallel-deps
Open

Meta: Stop building vcpkg dependencies serially#10963
alimpfard wants to merge 1 commit into
LadybirdBrowser:masterfrom
alimpfard:vcpkg-parallel-deps

Conversation

@alimpfard

Copy link
Copy Markdown
Contributor

Measured on two boxes (32x 9950x + 128GB@4800MT/s and 24x Ultra 9 275HX + 32GB @ 5600MT/s) plus icecc cluster with the two.

cores [*=icecc] phase before after HOWFAST
- fetch 36s 36s 1x
32 build 940s 253s 3.7x
24 build 631s 306s 2.1x
56* build 466s 165s 2.8x

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The PR adds a vcpkg dependency builder that creates per-port Ninja nodes, manages caches and concurrency, and supports fetch and rebuild modes. Ladybird gains a deps command and staged dependency configuration on non-Windows hosts.

Vcpkg dependency build

Layer / File(s) Summary
Platform and CLI setup
Meta/Utils/vcpkg_deps.py, Meta/ladybird.py
Adds platform triplet selection, overlay and cache paths, vcpkg environment setup, shared arguments, and the deps and run-node commands.
Dependency graph and Ninja generation
Meta/Utils/vcpkg_deps.py
Parses vcpkg depend-info, builds dependency closures and standalone manifests, and generates per-port Ninja rules.
Node scheduling and execution
Meta/Utils/vcpkg_deps.py
Adds heavy-port classification, FIFO token scheduling, isolated build roots, per-node logs, completion stamps, and stale-install checks.
Dependency orchestration and staged configuration
Meta/Utils/vcpkg_deps.py, Meta/ladybird.py
Adds fetch, rebuild, locking, cache restore, Ninja execution, and non-Windows staged dependency configuration.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant LadybirdCLI
  participant build_dependencies
  participant vcpkg
  participant Ninja
  participant run_node
  participant BinaryCache
  LadybirdCLI->>build_dependencies: configure dependency build
  build_dependencies->>vcpkg: query dependency graph
  build_dependencies->>Ninja: generate per-port build graph
  Ninja->>run_node: execute dependency node
  run_node->>vcpkg: install isolated port
  vcpkg->>BinaryCache: restore or store package
  run_node-->>Ninja: write completion stamp
  build_dependencies-->>LadybirdCLI: return staged triplet
Loading
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description reports dependency build performance improvements that directly relate to the parallel vcpkg dependency changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (3)
Meta/Utils/vcpkg_deps.py (3)

266-281: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Node inputs omit the overlay ports of the dependency closure.

Line 272 adds only the overlay port files for name. Each node's manifest, however, builds the whole closure returned by dependency_closure. If you edit an overlay port for a dependency, that dependency's ABI hash changes, but the dependent node's .stamp stays up to date and ninja does not re-run it. The dependent is then rebuilt serially by the final manifest install instead of in parallel.

Extend the inputs to cover the overlay ports of every port in the closure.

♻️ Proposed change
-        overlay_port = overlay_ports_dir() / name
-        if overlay_port.is_dir():
-            inputs.extend(sorted(str(f) for f in overlay_port.rglob("*") if f.is_file()))
+        for closure_port in sorted(dependency_closure(graph, name)):
+            overlay_port = overlay_ports_dir() / closure_port
+            if overlay_port.is_dir():
+                inputs.extend(sorted(str(f) for f in overlay_port.rglob("*") if f.is_file()))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Meta/Utils/vcpkg_deps.py` around lines 266 - 281, Update the node input
construction around dependency_closure and overlay_port so each node includes
files from the overlay port directories of every port in its dependency closure,
not only name. Preserve the existing sorted file ordering and include these
paths in the Ninja build inputs so changes to any closure dependency invalidate
the dependent node.

486-487: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

A failed dry run is treated as "work to do".

subprocess.run at line 486 captures output but never checks returncode. If build.ninja is malformed or ninja is missing, check.stdout is empty, have_work becomes True, and the real error surfaces much later from the check_call at line 507. Check check.returncode and report the captured stderr.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Meta/Utils/vcpkg_deps.py` around lines 486 - 487, Update the dry-run
subprocess handling in the dependency build flow to validate check.returncode
before deriving have_work. When ninja fails, report the captured check.stderr
and stop with an appropriate error instead of treating the failure as pending
work; preserve the existing no-work detection for successful runs.

360-410: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Bind heavy unconditionally to make the log path robust.

heavy is assigned only inside the if token_fifo ... block at line 383. Line 392 reads it. The if held: guard at line 391 keeps this safe today, because held is non-zero only when the block ran. The two variables are set in different places, so a later change to the token logic can turn this into a NameError.

♻️ Proposed change
     fifo_fd = None
     held = 0
+    heavy = False
     token_fifo = environment.get("LADYBIRD_VCPKG_TOKEN_FIFO")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Meta/Utils/vcpkg_deps.py` around lines 360 - 410, Initialize heavy
unconditionally before the token FIFO block in run_node, using a false/default
value, then retain the existing weighted assignment when the token pool logic
runs. Ensure the held-token logging path can always reference heavy without
risking an unbound local variable.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Meta/ladybird.py`:
- Around line 194-200: The deps branch in Meta/ladybird.py lines 194-200 must
reject HostSystem.Windows before calling build_dependencies, exiting with a
message that staged dependency builds are unsupported on Windows. In
Meta/Utils/vcpkg_deps.py lines 466-468, enforce the same restriction at the
build_dependencies boundary or move the fcntl import behind a platform guard so
Windows cannot reach POSIX-only code.

In `@Meta/Utils/vcpkg_deps.py`:
- Around line 492-507: Update the ninja invocation in the token-pool setup to
always pass the resolved `tokens` job count, including when `jobs` is unset.
Replace the conditional `-j` argument handling in the `ninja_arguments`
construction while preserving the existing resolved count and environment
configuration.
- Around line 198-207: Update dependency_closure to handle dependencies missing
from graph without raising an unhandled KeyError: when processing current,
verify it exists in graph before accessing graph[current].dependencies, and
provide contextual failure handling for the absent port while preserving closure
tracking.
- Around line 169-195: Update query_dependency_graph to preserve a separator
when combining result.stdout and result.stderr, then validate that the parsed
graph is non-empty after processing the dependency-info output. Raise a clear
RuntimeError when no dependency entries are parsed, including the command-output
context needed to diagnose a format change, instead of returning an empty graph.
- Around line 102-114: Update extra_vcpkg_variables so the macOS branch emits
VCPKG_OSX_DEPLOYMENT_TARGET only when CMAKE_OSX_DEPLOYMENT_TARGET is set,
matching generate_vcpkg_toolchain_variables.cmake. Remove the unconditional 14.0
fallback and preserve the existing behavior for non-macOS platforms.
- Around line 334-357: Update acquire_build_tokens so heavy-node acquisition is
all-or-nothing and cannot leave tokens stranded while waiting for target; use a
dedicated lock to serialize the heavy acquisition path, then acquire the
required tokens before allowing other heavy nodes to contend. Preserve the
existing light-node behavior and nonblocking cap logic, and ensure the lock is
released on every exit.

---

Nitpick comments:
In `@Meta/Utils/vcpkg_deps.py`:
- Around line 266-281: Update the node input construction around
dependency_closure and overlay_port so each node includes files from the overlay
port directories of every port in its dependency closure, not only name.
Preserve the existing sorted file ordering and include these paths in the Ninja
build inputs so changes to any closure dependency invalidate the dependent node.
- Around line 486-487: Update the dry-run subprocess handling in the dependency
build flow to validate check.returncode before deriving have_work. When ninja
fails, report the captured check.stderr and stop with an appropriate error
instead of treating the failure as pending work; preserve the existing no-work
detection for successful runs.
- Around line 360-410: Initialize heavy unconditionally before the token FIFO
block in run_node, using a false/default value, then retain the existing
weighted assignment when the token pool logic runs. Ensure the held-token
logging path can always reference heavy without risking an unbound local
variable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f748dcfc-8c80-4710-bf28-dfea367991cf

📥 Commits

Reviewing files that changed from the base of the PR and between 12176d0 and 2fbbec3.

📒 Files selected for processing (2)
  • Meta/Utils/vcpkg_deps.py
  • Meta/ladybird.py

Comment thread Meta/ladybird.py
Comment on lines +194 to +200
elif args.command == "deps":
_, build_preset_dir = configure_build_env(platform, args.preset, args.jobs)
build_vcpkg()
(cc, cxx) = pick_host_compiler(platform, args.cc, args.cxx)
build_dependencies(
platform, args.preset, build_preset_dir, cc, cxx, args.jobs, fetch_only=args.fetch, rebuild=args.rebuild
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

The deps command reaches POSIX-only code on Windows. build_dependencies depends on fcntl and os.mkfifo, which do not exist on Windows. configure_main guards the call with staged_deps, but the deps subcommand does not, so ./ladybird.py deps on Windows fails with ModuleNotFoundError: No module named 'fcntl'.

  • Meta/ladybird.py#L194-L200: add a platform.host_system == HostSystem.Windows check in the deps branch and exit with a message that staged dependency builds are not supported on Windows.
  • Meta/Utils/vcpkg_deps.py#L466-L468: move import fcntl to a guarded location, or raise an explicit RuntimeError at the top of build_dependencies when the host system is Windows, so the constraint is enforced at the function boundary.
📍 Affects 2 files
  • Meta/ladybird.py#L194-L200 (this comment)
  • Meta/Utils/vcpkg_deps.py#L466-L468
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Meta/ladybird.py` around lines 194 - 200, The deps branch in Meta/ladybird.py
lines 194-200 must reject HostSystem.Windows before calling build_dependencies,
exiting with a message that staged dependency builds are unsupported on Windows.
In Meta/Utils/vcpkg_deps.py lines 466-468, enforce the same restriction at the
build_dependencies boundary or move the fcntl import behind a platform guard so
Windows cannot reach POSIX-only code.

Comment thread Meta/Utils/vcpkg_deps.py
Comment on lines +102 to +114
def extra_vcpkg_variables(platform: Platform, cc: str, cxx: str) -> str:
# The data generated by this must be exactly identical to the one generated by Meta/CMake/vcpkg/generate_vcpkg_toolchain_variables.cmake;
# as vcpkg will use the identity of that as part of the ABI hash.
variables = ""
if platform.host_system != HostSystem.Windows:
variables += f"set(ENV{{CC}} {cc})\n"
variables += f"set(ENV{{CXX}} {cxx})\n"
if platform.host_system == HostSystem.Linux and not os.environ.get("LAGOM_USE_LINKER"):
variables += "set(ENV{LDFLAGS} -Wl,-z,noseparate-code)\n"
if platform.host_system == HostSystem.macOS:
deployment_target = os.environ.get("CMAKE_OSX_DEPLOYMENT_TARGET") or "14.0"
variables += f"set(VCPKG_OSX_DEPLOYMENT_TARGET {deployment_target})\n"
return variables

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Compare the generated variable content with the CMake generator.
fd -t f 'generate_vcpkg_toolchain_variables.cmake' Meta | xargs -r cat -n

Repository: LadybirdBrowser/ladybird

Length of output: 4547


🏁 Script executed:

#!/bin/bash
set -u

echo "== locate vcpkg_deps.py =="
fd -t f 'vcpkg_deps.py' Meta | sed "s#^\./##"

echo "== relevant vcpkg_deps.py section =="
python3 - <<'PY'
from pathlib import Path
for p in Path('Meta').rglob('vcpkg_deps.py'):
    print(f"--- {p} ---")
    lines = p.read_text().splitlines()
    for i in range(94, 150):
        if i <= len(lines):
            print(f"{i:4}: {lines[i-1]}")
PY

echo "== search for extra_vcpkg_variables call =="
rg -n "extra_vcpkg_variables|build-vcpkg-variables|vcpkg_deps" Meta -S

Repository: LadybirdBrowser/ladybird

Length of output: 4511


🏁 Script executed:

#!/bin/bash
set -u
python3 - <<'PY'
from pathlib import Path

cmake = Path("Meta/CMake/vcpkg/generate_vcpkg_toolchain_variables.cmake").read_text().splitlines()
python_fn = Path("Meta/Utils/vcpkg_deps.py").read_text().splitlines()[101:114]  # zero-indexed lines 102-114

def cmake_lines(s):
    out = []
    for line in s.splitlines():
        if "string(APPEND EXTRA_VCPKG_VARIABLES" in line or "file(WRITE" in line:
            out.append(line.strip())
    return out

print("CMake append/write block:")
for line in cmake_lines(" ".join(cmake[3:27])):
    print(line)

print("\nPython extra_vcpkg_variables() body:")
for line in python_fn:
    print(line)
PY

Repository: LadybirdBrowser/ladybird

Length of output: 2119


Align macOS coverage between the Python and CMake variable generators.

Meta/CMake/vcpkg/generate_vcpkg_toolchain_variables.cmake writes VCPKG_OSX_DEPLOYMENT_TARGET when CMAKE_OSX_DEPLOYMENT_TARGET is set, while Meta/Utils/vcpkg_deps.py always writes it with a 14.0 fallback. If the CMake generator runs without that environment variable, the bytes written for this entry differ, which can invalidate the vcpkg ABI hash.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Meta/Utils/vcpkg_deps.py` around lines 102 - 114, Update
extra_vcpkg_variables so the macOS branch emits VCPKG_OSX_DEPLOYMENT_TARGET only
when CMAKE_OSX_DEPLOYMENT_TARGET is set, matching
generate_vcpkg_toolchain_variables.cmake. Remove the unconditional 14.0 fallback
and preserve the existing behavior for non-macOS platforms.

Comment thread Meta/Utils/vcpkg_deps.py
Comment on lines +169 to +195
if result.returncode != 0:
print(result.stdout, file=sys.stderr)
print(result.stderr, file=sys.stderr)
raise RuntimeError("vcpkg depend-info failed")

# NOTE: This folds host and target entries, same as what vcpkg does.
# FIXME: Investigate when cross-compilation becomes a problem this has to touch.
graph: dict[str, PortNode] = {}
for line in (result.stdout + result.stderr).splitlines():
match = DEPEND_INFO_REGEX.match(line.strip())
if not match:
continue
(name, features, _, dependencies) = match.groups()

node = graph.setdefault(name, PortNode(name))
if features:
node.features.update(f for f in (feature.strip() for feature in features.split(",")) if f and f != "core")

for dependency in dependencies.split(","):
dependency = dependency.strip()
if not dependency:
continue
dependency = dependency.split("[")[0].removesuffix(":host")
if dependency != name:
node.dependencies.add(dependency)

return graph

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Add a guard for an empty dependency graph.

query_dependency_graph only fails on a non-zero exit code. If the depend-info --format=list output format changes, every line fails the regex and the function returns an empty graph. The build then generates an empty build.ninja, reports "no work to do", and falls back to a single serial manifest install. The parallel build is lost with no error.

Also, result.stdout + result.stderr concatenates without a separator. If stdout does not end with a newline, the last stdout line and the first stderr line merge into one unparsable line.

🛡️ Proposed guard
     graph: dict[str, PortNode] = {}
-    for line in (result.stdout + result.stderr).splitlines():
+    for line in (result.stdout.splitlines() + result.stderr.splitlines()):
         match = DEPEND_INFO_REGEX.match(line.strip())
         if not match:
             continue
@@
+    if not graph:
+        raise RuntimeError("vcpkg depend-info returned no parsable dependency entries")
+
     return graph
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if result.returncode != 0:
print(result.stdout, file=sys.stderr)
print(result.stderr, file=sys.stderr)
raise RuntimeError("vcpkg depend-info failed")
# NOTE: This folds host and target entries, same as what vcpkg does.
# FIXME: Investigate when cross-compilation becomes a problem this has to touch.
graph: dict[str, PortNode] = {}
for line in (result.stdout + result.stderr).splitlines():
match = DEPEND_INFO_REGEX.match(line.strip())
if not match:
continue
(name, features, _, dependencies) = match.groups()
node = graph.setdefault(name, PortNode(name))
if features:
node.features.update(f for f in (feature.strip() for feature in features.split(",")) if f and f != "core")
for dependency in dependencies.split(","):
dependency = dependency.strip()
if not dependency:
continue
dependency = dependency.split("[")[0].removesuffix(":host")
if dependency != name:
node.dependencies.add(dependency)
return graph
if result.returncode != 0:
print(result.stdout, file=sys.stderr)
print(result.stderr, file=sys.stderr)
raise RuntimeError("vcpkg depend-info failed")
# NOTE: This folds host and target entries, same as what vcpkg does.
# FIXME: Investigate when cross-compilation becomes a problem this has to touch.
graph: dict[str, PortNode] = {}
for line in (result.stdout.splitlines() + result.stderr.splitlines()):
match = DEPEND_INFO_REGEX.match(line.strip())
if not match:
continue
(name, features, _, dependencies) = match.groups()
node = graph.setdefault(name, PortNode(name))
if features:
node.features.update(f for f in (feature.strip() for feature in features.split(",")) if f and f != "core")
for dependency in dependencies.split(","):
dependency = dependency.strip()
if not dependency:
continue
dependency = dependency.split("[")[0].removesuffix(":host")
if dependency != name:
node.dependencies.add(dependency)
if not graph:
raise RuntimeError("vcpkg depend-info returned no parsable dependency entries")
return graph
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Meta/Utils/vcpkg_deps.py` around lines 169 - 195, Update
query_dependency_graph to preserve a separator when combining result.stdout and
result.stderr, then validate that the parsed graph is non-empty after processing
the dependency-info output. Raise a clear RuntimeError when no dependency
entries are parsed, including the command-output context needed to diagnose a
format change, instead of returning an empty graph.

Comment thread Meta/Utils/vcpkg_deps.py
Comment on lines +198 to +207
def dependency_closure(graph: dict[str, PortNode], port: str) -> set[str]:
closure: set[str] = set()
queue = [port]
while queue:
current = queue.pop()
if current in closure:
continue
closure.add(current)
queue.extend(graph[current].dependencies)
return closure

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle dependency names that are absent from the graph.

graph[current] raises KeyError if a dependency name appears on the right-hand side of a depend-info line but never gets its own parsed line. DEPEND_INFO_REGEX only accepts [a-zA-Z0-9-]+ for the port name, so any port whose line does not match is missing as a key while still being referenced as a dependency. The failure surfaces as an unhandled KeyError with no context.

🛡️ Proposed fix
     while queue:
         current = queue.pop()
         if current in closure:
             continue
+        if current not in graph:
+            raise RuntimeError(f"vcpkg depend-info referenced unknown port {current!r}")
         closure.add(current)
         queue.extend(graph[current].dependencies)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def dependency_closure(graph: dict[str, PortNode], port: str) -> set[str]:
closure: set[str] = set()
queue = [port]
while queue:
current = queue.pop()
if current in closure:
continue
closure.add(current)
queue.extend(graph[current].dependencies)
return closure
def dependency_closure(graph: dict[str, PortNode], port: str) -> set[str]:
closure: set[str] = set()
queue = [port]
while queue:
current = queue.pop()
if current in closure:
continue
if current not in graph:
raise RuntimeError(f"vcpkg depend-info referenced unknown port {current!r}")
closure.add(current)
queue.extend(graph[current].dependencies)
return closure
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Meta/Utils/vcpkg_deps.py` around lines 198 - 207, Update dependency_closure
to handle dependencies missing from graph without raising an unhandled KeyError:
when processing current, verify it exists in graph before accessing
graph[current].dependencies, and provide contextual failure handling for the
absent port while preserving closure tracking.

Comment thread Meta/Utils/vcpkg_deps.py
Comment on lines +334 to +357
def acquire_build_tokens(fifo_fd: int, pool: int, heavy: bool) -> int:
# For light nodes, a single token is sufficient, so they get dispatched as soon as there's one free and run at whatever parallelism is there.
# heavy nodes need guaranteed available compute to not thrash on the build, so they hold off until there are enough tokens to guarantee a parallel build.
held = len(os.read(fifo_fd, 1))
if not heavy:
return held

target = max(2, pool // 4)
while held < target:
held += len(os.read(fifo_fd, 1))

cap = max(target, pool - 8)
os.set_blocking(fifo_fd, False)
try:
while held < cap:
extra = os.read(fifo_fd, cap - held)
if not extra:
break
held += len(extra)
except BlockingIOError:
pass
finally:
os.set_blocking(fifo_fd, True)
return held

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

Heavy nodes can deadlock on partial token acquisition.

A heavy node takes one token at line 337, then blocks at line 343 until it reaches target. It never releases the tokens it already holds. If enough heavy nodes start together, every token in the pool is held by a node that is still waiting for more, and no node can make progress.

The case is reachable. With pool = 8, target = max(2, 8 // 4) = 2. If ninja starts 8 heavy nodes, each one reads a single token and then blocks forever. The situation is more likely when jobs is None: line 492 sets tokens = os.cpu_count(), while ninja's default parallelism is higher than the core count, so more nodes run than there are tokens.

Make the acquisition all-or-nothing. Return the partially held tokens and retry, or serialize heavy acquisition behind a separate lock.

🔒 One approach: release and retry instead of holding a partial set
 def acquire_build_tokens(fifo_fd: int, pool: int, heavy: bool) -> int:
     held = len(os.read(fifo_fd, 1))
     if not heavy:
         return held
 
     target = max(2, pool // 4)
-    while held < target:
-        held += len(os.read(fifo_fd, 1))
+    # Drain what is available without blocking; if we cannot reach the target,
+    # give everything back so another node can make progress, then retry.
+    while held < target:
+        os.set_blocking(fifo_fd, False)
+        try:
+            while held < target:
+                extra = os.read(fifo_fd, target - held)
+                if not extra:
+                    break
+                held += len(extra)
+        except BlockingIOError:
+            pass
+        finally:
+            os.set_blocking(fifo_fd, True)
+        if held < target:
+            os.write(fifo_fd, b"x" * held)
+            held = 0
+            time.sleep(0.5)
+            held = len(os.read(fifo_fd, 1))

Note: this sketch needs an import time and still allows livelock under contention. A dedicated heavy-node lock, so that only one heavy node acquires tokens at a time, is the more robust fix.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Meta/Utils/vcpkg_deps.py` around lines 334 - 357, Update acquire_build_tokens
so heavy-node acquisition is all-or-nothing and cannot leave tokens stranded
while waiting for target; use a dedicated lock to serialize the heavy
acquisition path, then acquire the required tokens before allowing other heavy
nodes to contend. Preserve the existing light-node behavior and nonblocking cap
logic, and ensure the lock is released on every exit.

Comment thread Meta/Utils/vcpkg_deps.py Outdated
Comment on lines +492 to +507
tokens = int(jobs) if jobs else (os.cpu_count() or 4)
token_fifo = deps_dir / ".tokens"
token_fifo.unlink(missing_ok=True)
os.mkfifo(token_fifo)
fifo_fd = os.open(token_fifo, os.O_RDWR)
os.write(fifo_fd, b"x" * tokens)

ninja_environment = os.environ.copy()
ninja_environment["LADYBIRD_VCPKG_TOKEN_FIFO"] = str(token_fifo)
ninja_environment["LADYBIRD_VCPKG_TOKEN_POOL"] = str(tokens)

try:
ninja_arguments = ["ninja", "-C", str(deps_dir)]
if jobs:
ninja_arguments.extend(["-j", jobs])
subprocess.check_call(ninja_arguments, env=ninja_environment)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

The ninja job count and the token pool disagree when jobs is unset.

If jobs is None, line 492 sizes the pool at os.cpu_count(), and line 504 starts ninja with no -j. Ninja then defaults to more than the core count, typically cores plus two. More nodes run concurrently than there are tokens. Every extra node blocks on the first os.read in acquire_build_tokens, and the risk of the heavy-node deadlock described on lines 334-357 rises.

Pass the resolved job count to ninja in both cases.

♻️ Proposed change
             tokens = int(jobs) if jobs else (os.cpu_count() or 4)
@@
             try:
-                ninja_arguments = ["ninja", "-C", str(deps_dir)]
-                if jobs:
-                    ninja_arguments.extend(["-j", jobs])
+                ninja_arguments = ["ninja", "-C", str(deps_dir), "-j", str(tokens)]
                 subprocess.check_call(ninja_arguments, env=ninja_environment)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
tokens = int(jobs) if jobs else (os.cpu_count() or 4)
token_fifo = deps_dir / ".tokens"
token_fifo.unlink(missing_ok=True)
os.mkfifo(token_fifo)
fifo_fd = os.open(token_fifo, os.O_RDWR)
os.write(fifo_fd, b"x" * tokens)
ninja_environment = os.environ.copy()
ninja_environment["LADYBIRD_VCPKG_TOKEN_FIFO"] = str(token_fifo)
ninja_environment["LADYBIRD_VCPKG_TOKEN_POOL"] = str(tokens)
try:
ninja_arguments = ["ninja", "-C", str(deps_dir)]
if jobs:
ninja_arguments.extend(["-j", jobs])
subprocess.check_call(ninja_arguments, env=ninja_environment)
tokens = int(jobs) if jobs else (os.cpu_count() or 4)
token_fifo = deps_dir / ".tokens"
token_fifo.unlink(missing_ok=True)
os.mkfifo(token_fifo)
fifo_fd = os.open(token_fifo, os.O_RDWR)
os.write(fifo_fd, b"x" * tokens)
ninja_environment = os.environ.copy()
ninja_environment["LADYBIRD_VCPKG_TOKEN_FIFO"] = str(token_fifo)
ninja_environment["LADYBIRD_VCPKG_TOKEN_POOL"] = str(tokens)
try:
ninja_arguments = ["ninja", "-C", str(deps_dir), "-j", str(tokens)]
subprocess.check_call(ninja_arguments, env=ninja_environment)
🧰 Tools
🪛 ast-grep (0.45.0)

[error] 506-506: Use of unsanitized data to create processes
Context: subprocess.check_call(ninja_arguments, env=ninja_environment)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(os-system-unsanitized-data)

🪛 Ruff (0.16.0)

[error] 507-507: subprocess call: check for execution of untrusted input

(S603)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Meta/Utils/vcpkg_deps.py` around lines 492 - 507, Update the ninja invocation
in the token-pool setup to always pass the resolved `tokens` job count,
including when `jobs` is unset. Replace the conditional `-j` argument handling
in the `ninja_arguments` construction while preserving the existing resolved
count and environment configuration.

@alimpfard
alimpfard force-pushed the vcpkg-parallel-deps branch from 2fbbec3 to 4878c72 Compare August 2, 2026 15:14
This is a massive waste of time if run serially, as all packages waste
significant time just downloading and configuring things.
Run them in parallel with a little pre-knowledge to go from 16m build
time to 3m (caches all cold, all sources already downloaded for both).
@alimpfard
alimpfard force-pushed the vcpkg-parallel-deps branch from 4878c72 to b59bcac Compare August 2, 2026 15:15

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
Meta/Utils/vcpkg_deps.py (2)

520-522: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document or guard the POSIX-only dependency.

build_dependencies needs fcntl at line 520 and os.mkfifo at line 557. Both are POSIX-only. The function-scope import hides this requirement from the reader and from the caller. Add a short comment or an explicit platform check at the top of the function.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Meta/Utils/vcpkg_deps.py` around lines 520 - 522, Make the POSIX-only
requirement explicit at the start of build_dependencies: add a concise platform
guard or documentation comment covering both fcntl and os.mkfifo before the
function uses them. Keep the existing locking and FIFO behavior unchanged.

413-426: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Read .weight once.

Lines 421-423 stat and read the same file in two steps and repeat the path expression. Also initialize heavy outside the block so line 432 does not depend on held being non-zero for the name to be bound.

♻️ Proposed refactor
     fifo_fd = None
     held = 0
+    heavy = False
     token_fifo = environment.get("LADYBIRD_VCPKG_TOKEN_FIFO")
     if token_fifo and Path(token_fifo).exists():
         pool = (
             int(environment.get("LADYBIRD_VCPKG_TOKEN_POOL", "0"))
             or (os.cpu_count() or 2) + 2
         )
-        heavy = (node_dir / ".weight").exists() and (
-            node_dir / ".weight"
-        ).read_text().strip() == "heavy"
+        weight_path = node_dir / ".weight"
+        heavy = weight_path.is_file() and weight_path.read_text().strip() == "heavy"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Meta/Utils/vcpkg_deps.py` around lines 413 - 426, In the token FIFO setup
around acquire_build_tokens, initialize heavy before the token_fifo conditional,
then read node_dir / ".weight" once and derive whether it equals "heavy" without
repeating the path or performing separate existence and read operations.
Preserve the existing concurrency behavior while ensuring heavy is always
defined for later use.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Meta/Utils/vcpkg_deps.py`:
- Around line 86-89: Update overlay_triplets_dir to validate the preset through
the same supported-preset handling used by the .get call near line 77, and raise
a clear error for unknown values that includes the supported preset names
instead of allowing a bare KeyError from PRESET_TRIPLET_DIRS[preset]. Preserve
the existing path construction for valid presets.

---

Nitpick comments:
In `@Meta/Utils/vcpkg_deps.py`:
- Around line 520-522: Make the POSIX-only requirement explicit at the start of
build_dependencies: add a concise platform guard or documentation comment
covering both fcntl and os.mkfifo before the function uses them. Keep the
existing locking and FIFO behavior unchanged.
- Around line 413-426: In the token FIFO setup around acquire_build_tokens,
initialize heavy before the token_fifo conditional, then read node_dir /
".weight" once and derive whether it equals "heavy" without repeating the path
or performing separate existence and read operations. Preserve the existing
concurrency behavior while ensuring heavy is always defined for later use.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 78df69f9-1aea-4ea0-ae3d-a3f546b52bbd

📥 Commits

Reviewing files that changed from the base of the PR and between 2fbbec3 and b59bcac.

📒 Files selected for processing (2)
  • Meta/Utils/vcpkg_deps.py
  • Meta/ladybird.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • Meta/ladybird.py

Comment thread Meta/Utils/vcpkg_deps.py
Comment on lines +86 to +89
def overlay_triplets_dir(preset: str) -> Path:
return (
LADYBIRD_SOURCE_DIR / "Meta" / "CMake" / "vcpkg" / PRESET_TRIPLET_DIRS[preset]
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Report an unknown preset with a clear error.

Line 77 tolerates an unknown preset through .get, but line 88 raises a bare KeyError. The user sees a traceback with no list of supported presets.

🛡️ Proposed fix
 def overlay_triplets_dir(preset: str) -> Path:
+    if preset not in PRESET_TRIPLET_DIRS:
+        raise RuntimeError(
+            f"Unknown preset {preset!r}; expected one of {sorted(PRESET_TRIPLET_DIRS)}"
+        )
     return (
         LADYBIRD_SOURCE_DIR / "Meta" / "CMake" / "vcpkg" / PRESET_TRIPLET_DIRS[preset]
     )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def overlay_triplets_dir(preset: str) -> Path:
return (
LADYBIRD_SOURCE_DIR / "Meta" / "CMake" / "vcpkg" / PRESET_TRIPLET_DIRS[preset]
)
def overlay_triplets_dir(preset: str) -> Path:
if preset not in PRESET_TRIPLET_DIRS:
raise RuntimeError(
f"Unknown preset {preset!r}; expected one of {sorted(PRESET_TRIPLET_DIRS)}"
)
return (
LADYBIRD_SOURCE_DIR / "Meta" / "CMake" / "vcpkg" / PRESET_TRIPLET_DIRS[preset]
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Meta/Utils/vcpkg_deps.py` around lines 86 - 89, Update overlay_triplets_dir
to validate the preset through the same supported-preset handling used by the
.get call near line 77, and raise a clear error for unknown values that includes
the supported preset names instead of allowing a bare KeyError from
PRESET_TRIPLET_DIRS[preset]. Preserve the existing path construction for valid
presets.

@ADKaster

ADKaster commented Aug 2, 2026

Copy link
Copy Markdown
Member

Perhaps a silly question (without looking at a much of the python code) but is there pre-existing discussion on the vcpkg repo (issues or discussions) about parallelism?

If there's a hidden flag to the vcpkg binary or some such it would be pretty silly to build our own graph to sort deps and build them in a safe way.

@alimpfard

Copy link
Copy Markdown
Contributor Author

Perhaps a silly question (without looking at a much of the python code) but is there pre-existing discussion on the vcpkg repo (issues or discussions) about parallelism?

They haven't even figured out how to download in parallel yet; there's also this discussion from 6 years ago that is "any updates" central: microsoft/vcpkg#19129

It don't exist Andrew :(

@ADKaster

ADKaster commented Aug 2, 2026

Copy link
Copy Markdown
Member

That's... Surprising. Their vcpkg-tool surely has a graph of dependencies. Partitioning it into up to N moderately equally sized subgraphs shouldn't be a giant amount of effort. But Oki. I'll take a look at this to see if any of the vcpkg wrangling were doing in cmake is improperly mirrored later today then

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.

2 participants