The Verification Venue · your own machine is the apparatus

The Cores Your Browser Won’t Admit To

Your browser will tell you how many processors you have. It is one integer, it arrives instantly, and the specification that defines it explicitly permits it to be wrong. Below, your machine is asked the same question in the only way that cannot be fudged: run a fixed amount of arithmetic, cut it into p pieces, and count the workers that were each worth a whole core.

The two numbers are not the same quantity and they are not usually the same integer. One is a policy decision inside a browser engine, and you can read the exact lines that make it. The other is a property of the silicon, published by the company that sold it to you, and the one API that exists is under no obligation to report it. Section 01 finds a single exception in three engines' source, on one platform, by a route that can silently fall back to the other number. Everything below is built out of that gap.

01 · the published numberWhat your browser says, and the line of code that says it

There is one API. It is navigator.hardwareConcurrency, and on this page load it says:

Read straight out of your browser

hardwareConcurrency

Engine this looks like

crossOriginIsolated

SharedArrayBuffer

Workload self test

The self test runs the shipped arithmetic over a fixed range, whole and cut seven ways, and checks the two agree. It is the same integer in every engine, which is what makes the rest of this page comparable across machines.

The standard is unusually candid about what that integer is worth. The HTML Living Standard says the getter And then, in the same section:

Those are the two halves of the honest position: it is an upper bound with an escape clause. What that comes to in practice is decided in three files. Here they are, quoted from the engines’ own repositories, committed alongside this page.

enginewhat the getter actually does

Read those three and the differences stop being subtle. Chromium reports what the operating system reports. Firefox reports the same, clamped by a preference whose shipped default is , except on macOS, where it first asks the system for the physical core count and uses the operating system count only if that call is unavailable or returns zero. That branch is the one place any shipping browser hands over the quantity the rest of this page has to work to recover, and it is one engine on one platform. And WebKit does not report a count at all: it returns one of two constants, and the comment above the line has been asking for the number to be raised since machines with more than eight cores were rare.

The same machine, five different amounts of it, three engines

This is the experiment that settles it. One computer, one moment, one browser process per cell, launched with a CPU affinity mask that hands it a known number of hardware threads. The question asked is only navigator.hardwareConcurrency. Nothing is timed.

threads the process may useChromiumFirefoxWebKit

02 · the instrumentCount the workers that were worth a core

Here is the whole method. Take one fixed amount of arithmetic. Give all of it to one worker and time it. Then cut the same job into two, three, up to as many pieces as your browser will give workers for, and time each. The speedup at p workers is the one worker time divided by the p worker time, and the marginal gain of the p-th worker is how much that speedup rose when it arrived, in units of one whole core.

A worker that landed on a core of its own contributes about 1. A worker that landed on the second thread of a core already in use contributes a fraction, because the two threads are sharing one set of execution units. A worker with nowhere to go contributes about 0. So count the workers that earned their place, in order, and you have counted cores.

The sweep

idle

This will use every processor you have, at full tilt, for the duration. Your fan may spin up. Stop ends it at once and prints nothing, and so does hiding this tab or leaving the page, because a page that holds your cores at full load in the background has stopped being a measurement. Nothing leaves the page either way: no timing is stored, sent or written down anywhere, and the arithmetic vanishes when you close the tab.

Press it three times, because the operating system gets a vote

The count above rests on the shape of the curve at small worker counts, and that shape depends on which processors your two workers landed on. A page cannot ask for a placement, cannot see the one it got, and on a machine with two threads per core the difference between the two answers is the whole finding: two workers on two different cores run twice as fast, and two workers on the two threads of a single core do not. Below are three passes of the identical measurement, taken one after another on a quiet machine.

The same measurement, three times

taken at2nd worker added3rd4thbest speeduprecoveredverdict

If it printed a number, go and check it. Your CPU’s manufacturer publishes both figures for the exact part you own: Intel calls them Total Cores and Total Threads on ARK, AMD lists # of CPU Cores and # of Threads, and Apple states the performance and efficiency core counts in the technical specifications for each chip. That published integer is the anchor. It is not a number this page can see, which is the point: you are the only person in this transaction who can complete the check.

03 · the thresholdWhere the rule is real, and where it is true by construction

“Worth a core” needs a number. The rule above counts a worker if it raised the speedup by at least of a core, and a threshold chosen by the person reporting the result is exactly the kind of thing that should make you suspicious. So here is the threshold on a slider, and underneath it every capture whose true answer is known independently, from the CPU affinity mask it was taken under.

Move the threshold and watch the known answers go wrong

Each row was captured with the process pinned to a known set of hardware threads, so its answer was known before any timing was taken. The rows marked held out were taken inside real browser engines and were used to choose nothing at all: the floor, and the window under it, come only from the unmarked rows. The marked ones are a test, and they are printed pass or fail.

capturewhat it was giventruthrecoveredgains per workerverdict

Every fitted capture correct for

Width of that window

Whole machine goes vacuous below

And collapses to one above

Both ends of that slider are traps, and naming them is the honest part. Drag it low enough and every worker passes, so the recovered count is always the browser’s own number and the comparison is true by construction: the page would agree with the API on every machine on earth and would have measured nothing. Drag it high enough and no worker ever passes, so every machine has one core. The rule is only worth anything in between, and the window is measured above rather than asserted.

04 · one worker at a timeThe serial fraction, and why it is not the headline

Amdahl’s model says a job with a serial fraction f can never go faster than 1/f, however many processors you throw at it: S(p) = 1 / (f + (1-f)/p). Solve that for f at a single measured point and you get a per point estimate of the serial fraction, usually called the Karp–Flatt metric: e(p) = (1/S(p) − 1/p) / (1 − 1/p). It is one line of algebra and it is enormously more useful than the fitted f, because a rising e is not noise. It is the machine telling you that the p-th processor was not like the ones before it.

Pick a capture and watch e(p) move

Each of these was taken with a CPU affinity mask that fixes the effective topology, so the shape of e(p) can be compared with an answer that was known in advance.

Given

Physical cores in that set

Recovered here

Pooled fit of f

Which claims a ceiling of

pfastest runS(p)addede(p)reading

Now the reason the fitted f is not printed as a headline anywhere on this page. A serial fraction was injected into the same harness, on the same machine: a known share of the work was forced through one worker and the rest was split, and the fit was asked to recover the number it was given.

Give the fit a serial fraction and see what it gives back

f injectedS(2)S(3)S(4)f fittederrorcores recovered

05 · five ways to make it lieTraces built to be believed and refused

A reduction that returns a confident integer from nonsense is not an instrument. These are five deliberately broken measurements. Four are committed captures, taken on the machine that took the honest ones; the fifth kind is run live in your own browser when you press the button, so you can watch your machine produce the lie and watch the same reduction reject it.

The committed controls

controlwhat is wrong with itbest speedupfitted frefused forcaught

Run them here, on your machine

not run
controlbest speedup herework dividedrefused forcaught
not run yet

The live controls use a short sweep, so their numbers are rougher than the committed ones. What has to hold is not the number but the verdict, and each row is asked for the same refusal its committed twin above got, read off that twin rather than typed in here. Note which rule catches the sleeper: it hands each worker a slice of the wait rather than the whole wait, so its speedup is about the number of workers and can never exceed the machine. No timing rule can dislike it. What catches it is the arithmetic, which no clock is involved in. The honest control is the other way round: if your machine is loaded or throttling it can be refused, and that row prints the reason instead of a verdict, because an instrument declining a bad moment is the instrument working.

The sharpest of them is the one where nothing is faked at all. In the racing control the work really is divided, every worker really does its slice, and the checksums really do add up. The only fault is in the driver: it stops its clock when the first worker answers instead of the last, and one slice is deliberately small. That is a real bug that real benchmark harnesses have, it produces a beautiful curve, and the arithmetic check cannot see it. Only the rule that speedup may not exceed the machine catches it.

06 · the instrument measuring itselfThe workload this page threw away

Before any of the above could be trusted, one thing had to be true: the same arithmetic has to take the same time twice. It did not. The first workload here was an ordinary integer mixing loop, and inside a worker its compiled speed is not a number, it is a sequence.

Forty rounds, two kernels, one worker

Rejected kernel, fastest

Rejected kernel, slowest

Which is a spread of

Shipped kernel, spread

kernelfastestslowestspreaddistinct levelswhat it is

A three-fold change in the speed of the thing being timed, arriving part way through, is larger than the whole effect this page measures. It cannot be averaged away and it cannot be warmed away in any reasonable time. The fix for an unstable instrument is a better instrument, not better statistics laid over the top of it, so the workload was replaced. The rejected loop is still in workload.mjs, exported and unused, because deleting it would hide how easily this page could have shipped a speedup curve made of compiler.

Replacing it was not enough, and the second half of this is worse than the first. A pool of workers does not compile in unison. Each one is its own runtime, tiering up on its own schedule, and for a while several of them sit at one speed while the rest sit at another. A sweep taken across a pool in that state compares a one worker run served by one compiler with a four worker run served by another, and calls the difference parallelism. Three consecutive passes of the identical measurement on this machine came back with recovered counts of one, one and two while that was happening.

So the warm-up does not count runs, and it does not trust an average. It probes every worker on its own, repeatedly, and refuses to start until they all agree about how fast this arithmetic is. Below is what that costs and what it buys, read off the committed captures themselves.

What each worker settled at, before anything was timed

captureroundseach worker, ns per iterationspreadagreed

07 · the other lawAsk a different question of the same silicon and get a different number

Everything so far holds the job fixed and cuts it up, which is the arrangement Amdahl was arguing about. There is a second arrangement, usually named after Gustafson’s 1988 note: hold the work per worker fixed and let the job grow. Nobody buys a bigger computer to run the same small job faster; they run a bigger job.

The arithmetic is elementary and is worth doing in front of you rather than citing. Suppose the measured run spends a fraction s of its wall time in a serial phase and the rest in a parallel phase spread over N workers. One worker doing that same grown job would spend the same s, and N times the parallel part, so it would take s + N(1-s) times as long. That ratio is the scaled speedup, and unlike Amdahl’s it has no ceiling at all: it rises without bound in N.

Weak scaling, same machine, same workload

workerswall timeone worker, same jobscaled speedups + N(1-s)shortfall

Measured at the widest

The law predicts

The gap

Physical cores on that machine

Both laws assume N equal, independent processors. The machine has not got N of those, and the shortfall is the same deficit the sweep found, arriving by a completely different route.

The paper the law is named after has no equations in it

Amdahl’s 1967 note is three pages long, it is an argument about buying mainframes, and the editors who reprinted it in 2007 noticed something worth repeating. Of the paper, they wrote: The formula everybody calls Amdahl’s law is not in Amdahl’s paper. What is in it is an estimate about housekeeping: data management overhead, he says, , and

That five to seven times is the most quoted number in the paper and it is not a ceiling on parallel speedup, so it is not compared to anything on this page. It is a 1967 estimate of one specific overhead in one specific kind of workload, and setting a modern laptop’s measured speedup beside it would be numerology. The paper supplies the method and the question. The number to check your machine against is the one your own vendor prints. The sentence the whole law is really named for is the last one:

The two laws, drawn, with your own points on them

The falling curve is Amdahl: a fixed job, cut p ways, ceiling 1/f. The straight line is the weak scaling arithmetic above at the same f. Your measured points appear once you have run the sweep.

Amdahl ceiling at this f

Amdahl at p = 64

Weak scaling at N = 64

f that best fits your points

The check, run in your browser, now

running…

Every figure above is recomputed from the same three modules the offline verifier imports. Rows marked RESTATEMENT only prove this page agrees with itself. Rows marked GROUND TRUTH, CONTROL and PROVENANCE are the ones that can actually go red, and rows marked OFFLINE: record something this browser cannot redo.

    The apparatus: what is being run, what is assumed, and what this page does not claim

    What is being run

    Three modules, one copy each. workload.mjs holds the arithmetic that gets timed. sweep.mjs holds the measurement protocol: the adaptive warm-up, the sizing of the job, the interleaved ascending and descending passes. scaling.mjs holds every threshold, the speedup, the marginal gain, the Karp–Flatt metric, the pooled fit and every refusal rule. All three are imported without modification by this page, by the offline mirror (research/how-many-cores-do-you-have/09-verify.mjs) and by the headless check (verify-how-many-cores-do-you-have.mjs). The only thing that differs between the browser and the capture harness is a single injected function that hands chunks to workers: Web Workers here, node worker_threads there.

    No SharedArrayBuffer is used and none is available: this site sends no Cross-Origin-Opener-Policy or Cross-Origin-Embedder-Policy headers, so crossOriginIsolated is false, which the panel at the top reports from your own browser. Message passing alone is enough. Real OS threads are real OS threads whether or not they share memory, and the speedup on two pinned physical cores comes out at .

    The rule fixed before measuring

    rulevaluechosen

    That is every threshold in the reduction, of them, and were chosen after seeing measurements: . Each says so in its own row, and the count in this sentence is counted from the table rather than typed into it. The marginal floor has its entire safe window published on the slider in section 03. The impossible serial fraction bound sits at 1.10 rather than at the principled 1.0 because the honest degenerate case, a single hardware thread, legitimately measures a fraction just over 1; the gap it sits in is measured, with the honest cases below and the duplicating control at . The busy floor sits at 1.5 in the same way, between the worst honest capture at and the deliberately loaded one at , and that is a narrower gap than the serial fraction bound gets.

    What this page does not claim

    Predictions, and how they did

    An error this page made and kept

    The first four rounds of captures are not in the repository, and they were all wrong in the same way: the job size was chosen from a single timing probe taken before the runtime had settled, so the sweep at one worker count ran at one compiled speed and the sweep at another ran at a different one. The visible symptom was a speedup below 1 at two workers on a machine with four hardware threads, which is not a subtle error and did not look like one. What makes it worth recording is that every check on the page would have passed: the arithmetic was right, the checksums added up, the reduction agreed with itself, the offline mirror agreed with the browser. Only the refusal rules noticed, and only because a serial fraction above 1 is impossible rather than merely surprising. The controls in section 05 are there because of that morning.

    Sources

    Every quotation on this page is pulled out of a file committed under research/how-many-cores-do-you-have/data/sources/ by 06-sources.mjs, which fails rather than let the page paraphrase.

    workrecord, and whether the full text was obtained

    Two of those three could not be read. Gustafson 1988 and Karp and Flatt 1990 are both behind the ACM Digital Library, which returns 403 to this machine, and no primary copy was found. So nothing is quoted from either, none of the figures those papers are famous for appear anywhere on this page, and the arithmetic attributed to them in sections 04 and 07 is derived here from Amdahl’s expression and from the definition of weak scaling rather than copied. Their bibliographic records were confirmed field by field against the publisher’s own Crossref metadata for each DOI, which is a check on the citation and not on the contents.

    The engines. Fetched from the three projects’ own repositories on the date recorded in the table above. Browser engines change; the page quotes the committed copies and both verifiers check the quotations against those copies, not against the network, so this page cannot start misquoting somebody because a file moved.

    Reproducing

    sh research/how-many-cores-do-you-have/00-fetch-sources.sh
    node research/how-many-cores-do-you-have/00-topology.mjs
    sh research/how-many-cores-do-you-have/run-ground-truth.sh
    sh research/how-many-cores-do-you-have/run-controls.sh
    node research/how-many-cores-do-you-have/03-engine-numbers.mjs
    node research/how-many-cores-do-you-have/04-calibration.mjs
    node research/how-many-cores-do-you-have/05-weak-scaling.mjs
    node research/how-many-cores-do-you-have/06-sources.mjs
    node research/how-many-cores-do-you-have/07-jit-tiering.mjs
    node research/how-many-cores-do-you-have/08-build-page-data.mjs
    node research/how-many-cores-do-you-have/09-verify.mjs
    node verify-how-many-cores-do-you-have.mjs

    Everything from 06 onward is offline and reproduces every number on this page from the committed files. The capture steps will produce your traces on your topology, not these ones, and the pinning in run-ground-truth.sh assumes the capture machine’s core layout, which is printed by 00-topology.mjs.