Engineering Practice

I Only Wanted the Tests to Run Faster

A contribution to V8 showed me what open source makes possible, and why shared code can take that further inside a company.

A hand uses tweezers to fit a small red component into an open mechanical assembly, with connected shafts and wheels extending across the workbench.

I went looking for the cause of a slow test suite and ended up with a fix merged into V8, the JavaScript engine behind Chrome and Node.js. From my Node bug report to both projects merging changes took about two and a half days. I am still rather excited about that.

For the wider argument, start with The Coordination Tax in Your Software Estate. This experience left me delighted by open-source collaboration and wondering why companies copy the separation that independent projects need, even when they own both sides.

InnerSource brings open-source ways of working inside a company. The code stays private, but colleagues can read it and propose changes across team boundaries. The team responsible still sets the direction, reviews contributions and decides what to merge.

My experience crossed three independently maintained projects. For anyone trying to make InnerSource work between teams in one company, there is plenty to celebrate here, and something to learn about which boundaries we actually need.

The problem kept leading somewhere else

Our tests ran under Vitest, a tool for running automated tests. Vitest, in turn, ran inside Node.js. Node uses V8 to execute JavaScript. A slowdown in the tests could come from any of those layers. I started with the one we used directly.

Vitest already had a known problem with mocks, the functions that stand in for real ones during a test. Andrew Brennan had reported it in January: as a test run continued, mocks accumulated, and clearing their state took longer. Scott Cooper changed Vitest to stop repeatedly scanning mocks that had already been cleared. His change merged on 2 September.

That improvement was in Vitest 5. We were on Vitest 4, so we needed to upgrade or bring the patch back to our version. But there was another question. Our tests had become slower after a Node upgrade. Vitest's repeated cleanup explained the amount of work; it did not explain why the same work now took longer.

I used AI to help investigate that question, and throughout the patch work that followed. I was not a specialist in the engine underneath Node. I still had to choose what to investigate, test the explanations and decide what the evidence justified. I remained responsible for what I submitted.

To separate Vitest's behaviour from Node's, I took the relevant mock code and removed pieces while checking that the slowdown remained. Eventually, a small JavaScript program with no dependencies reproduced it. The program did not need Vitest at all. Changing the Node version was enough to make it fast or slow.

That gave me a much smaller experiment. I compared Node releases and found the jump between 26.3.1 and 26.4.0. Nightly builds narrowed it to a single day's changes. Building Node before and after the suspect change confirmed which change exposed the slowdown.

Then I measured where the extra time went. It was inside V8, the engine executing the program, when it wrote values into objects. V8 keeps internal descriptions of object structure so it can reuse work. In the slow case, objects that had shared those descriptions were getting separate ones.

I traced that loss of reuse to two kinds of object competing for the same entry in a small V8 cache. Each kept displacing the other. Both Node builds contained the same V8 version, but the Node change had altered the engine's initial state so that this particular pair collided. The upgrade had exposed an engine weakness.

Now the two parts made sense together. Vitest was doing unnecessary cleanup, and V8 was making that cleanup more expensive. Reducing the work in Vitest offered a way around the slowdown. Repairing the cache in V8 could also help other code that encountered the same behaviour.

I proposed changing how V8 chose the cache entry and added a regression test. On 13 September, I submitted the V8 patch and documented the reproduction and diagnosis in Node issue #66011. Maintainers could check the reasoning without our application or test suite.

The submission had to earn their time. A plausible explanation and generated code would not have been enough. The reproduction, comparisons between builds and regression test gave them something concrete to assess.

During review, Leszek Swirski suggested using V8's existing hash-combining function. I revised the patch and tested it more widely. I shared the unfavourable results too, so the reviewers could judge the trade-off. Leszek and Jakob Kummerow approved the change, the landing checks ran, and the fix and regression test merged.

The affected loop in the local reproduction went from roughly 50 to 6 nanoseconds per item, about eightfold for that operation. The benefit to an application depends on how much time it spends doing that work.

I had followed a problem into an engine I had previously only used. Its maintainers helped improve the repair and kept the decision over what entered their project. I could contribute without joining their team or taking over their responsibilities.

A small repair in a widely shared engine

Part of the excitement is where the repair can travel. V8 runs JavaScript in Chrome and Node.js. It powers Cloudflare Workers, and Electron bundles it into desktop software, including Visual Studio Code and Slack. The same engine sits underneath remarkably different parts of an ordinary working day.

Those products will not all encounter this particular slowdown. But as they adopt versions containing the fix, affected code can benefit without its authors repeating our investigation. Something that began with our tests has a route into software far beyond our application. I find that wonderful.

Four patches across three layers

Back in our own test suite, I had also ported the Vitest improvement to Vitest 4 and tested it that Sunday, 13 September. A colleague then upgraded us to Vitest 5, so we no longer needed that patch. The version boundary had already created work, even though an upgrade eventually removed our need to carry it.

While the V8 work proceeded, Matteo Collina made a separate change in Node. It avoided the immediate trigger by changing when some internal symbols were created. My V8 patch addressed the cache hash directly.

Someone asked whether the Node change merely moved the problem, leaving it liable to return after a later change. Matteo explained why he was proceeding:

“However we can't assume when the V8 fix is going to land, and this seems better than reverting the ffi change.”

That is a sound decision for an independent project. Node is responsible for its users and cannot set V8's priorities or release schedule. It needed a remedy it could control while the engine change was still being reviewed.

Look across the whole chain and there are four patches across three layers: Scott's Vitest improvement, my Vitest 4 backport, Matteo's Node mitigation and my V8 repair. Vitest reduced the work. Node avoided the trigger. V8 repaired the cache behaviour underneath. The engine defect belonged in V8, but projects above it had good reasons to help their own users sooner.

The Vitest improvement has value of its own; these were four pieces of implementation work rather than four copies of one fix. Each offered a route to relief through a different version or project. The collaboration worked, and the separate release paths still required extra effort.

One slow test suite led to four patches across three layers: Scott Cooper’s Vitest 5 improvement, my tested Vitest 4 backport, Matteo Collina’s Node mitigation and my V8 cache repair. The figure shows the questions, evidence and expert review behind the contribution. Dated records distinguish the backport from merged changes; Node and V8 merged within about 59 hours of the Node report. A separate, illustrative comparison shows how a company with shared source and integration can retain review and ownership while avoiding intermediate internal package upgrades.
Four pieces of implementation work, with different roles. Merge dates come from the public record; the backport date comes from my recollection and same-day report. The company comparison illustrates a choice about shared source and integration. Sources: Vitest improvement, my backport, Node report, Node mitigation and V8 review and repair.

This is the coordination tax

That extra effort is the coordination tax: getting a shared change through separate versions, release schedules and adoption decisions. The merged V8 repair still has to pass through those steps.

Node can acquire the change through a V8 upgrade or a cherry-pick into an existing release line. A cherry-pick need not wait for a new V8 release, but it still needs review and testing. Node must then ship the resulting runtime and its users must adopt it. Node documents those routes.

Vitest needs no further release for existing tests to benefit from a fixed Node runtime. I have asked whether a Node 26 cherry-pick would be useful; there is no commitment yet. Matteo's mitigation gives Node its own route to relief in the meantime.

This is entirely legitimate in open source. Independent projects serve different consumers, answer to different maintainers and cannot assume everyone will move together. Their freedom to evolve separately is part of what makes the ecosystem work.

One company can make the next step easier

But why recreate that separation between teams when we own the code on both sides? Team ownership does not require a separate package release, version negotiation and upgrade project for every shared repair. For our own interdependent code, that is work we choose to impose on ourselves.

InnerSource gives us the contribution path I experienced here: bring evidence and a proposed improvement, then work with the owners to make it fit. The contributor owes the owning team a considered, tested proposal and the willingness to revise it. Owners bring the wider context and judgement over whether it belongs. That takes review capacity, but it does not require treating teams as independent suppliers.

Consider the internal equivalent: a shared component, a layer that consumes it and an application that exposes the fault. In a monorepo where those consumers build against the same shared source, a contributor can fix the component and test the affected application against that revision. The relevant owners can review the work together.

When the shared fix merges, the next consumer build can contain it. There is no need to publish an intermediate internal package and ask every consuming team to schedule its own upgrade. That can also remove the reason for a temporary implementation downstream while another team prepares its release.

A monorepo has to be operated that way. Putting separately pinned copies and independent integration queues into one directory preserves much of the delay. Builds and release controls remain, and changes across services may need staged deployment. External runtimes still need to be adopted too: moving our application code into a monorepo would not make V8 part of our own release authority.

We should keep an internal release boundary because the software needs it, not simply because another team owns it. Companies can embrace the openness I found in this experience, then use the advantage independent projects do not have: we can change our own shared code and its consumers together. We do not have to organise ourselves as strangers exchanging releases.

I only wanted the tests to run faster. I came away with a small contribution to V8, a great deal of gratitude to its maintainers and less patience for companies that make this harder than it needs to be.

Continue from here

Follow a related thread.

These essays continue the same argument.

Latest in this thread

Faux InnerSource

A contribution process is not InnerSource merely because another team can submit code. The test is whether repeated contributions build reusable engineering capability or repeatedly enter the same queue.

7 min read · InnerSource

The Code Belongs to Everyone

Inner sourcing only works when making changes is easy. Standards make that possible.

4 min read · InnerSource

Continue from here

Follow a related thread.

Selected automatically from shared topics, newest first.

Latest related essay

The Coordination Tax in Your Software Estate

Repository fragmentation turns shared work into internal transactions. Pricing those transactions reveals opportunities to move faster, release capacity and simplify control.

5 min read · Monorepo

Faux InnerSource

A contribution process is not InnerSource merely because another team can submit code. The test is whether repeated contributions build reusable engineering capability or repeatedly enter the same queue.

7 min read · InnerSource

Nx Should Make Code Graphs Cacheable

Nx already owns the Project Graph and distributed cache. Should it make finer Code Graph indexes reusable across the organisation?

6 min read · Monorepo

Browse by question

What are you trying to decide?

Find an argument

Search the shelf.

Search all published essays by title and standfirst.

New arguments, occasionally

Worth sending.

No cadence theatre. No filler. Just new essays when the argument is ready.