pcov: line coverage for PHP without loading a debugger
PCOV - CodeCoverage compatible driver for PHP
At a glance
- What is it?
- pcov is a small C extension that plugs into CodeCoverage and records line coverage by overriding the PHP executor, at the cost of ruling out Xdebug, phpdbg and Blackfire in the same process. It is worth adopting for CI speed, and wrong for anyone who needs path coverage.
- Who is it for?
- pcov suits teams running large PHP test suites in CI who want CodeCoverage output at low overhead and can live without Xdebug, phpdbg or Blackfire in the same run, and who are willing to read the LICENSE file, since GitHub cannot classify the licence. It does not suit anyone who needs path coverage, concurrent debug tooling, or a binary install without a source build.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 95 days ago.
- What is it written in?
- Mainly C, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A coverage driver that is deliberately not a debugger
pcov describes itself as a self contained CodeCoverage compatible driver for PHP, and the implementation is written in C. The repository root is small and readable: pcov.c, php_pcov.h, config.m4, config.w32, a cfg directory, a tests directory, composer.json and an INSTALL.md. The default branch is develop, and the primary language is C, so the reader is not looking at a framework written on top of another framework but at an extension that reaches into the engine.
The problem it solves is narrow and real. Xdebug is first and foremost a debugging extension, and the README is blunt about the consequence: when you load it, you incur the overhead of a debugger even when it is disabled. Teams that only need line coverage during a test run pay for a stack of tooling they never touch. pcov is described as less than 1000 lines of code, not including the control flow graph, and it has no debugger overhead to amortise.
The audience is therefore engineers wiring coverage into CI, or anyone running a test suite locally where startup cost matters. The repository has no GitHub releases and no listed homepage, so distribution is not the polished part. The README sends installation readers to INSTALL.md rather than spelling out steps, and the presence of config.m4 and config.w32 indicates a source build rather than a binary package.
Overriding the executor is what makes pcov work and what makes it exclusive
The mechanism is stated plainly in the interoperability notes: at an internals level, the executor function is overridden by pcov, so any extension or SAPI which does the same will be broken. Coverage cannot be gathered by watching from outside the engine, so pcov takes the engine's own execution hook for itself.
That single decision produces the hard constraint of the whole project. With pcov.enabled=1, the README lists interoperability with Xdebug, interoperability with phpdbg, and interoperability with the Blackfire profiler as all not possible. With pcov.enabled=0 the picture inverts: PCOV is zero cost, code runs at full speed, Xdebug may be loaded, phpdbg may be executed, the Blackfire probe may be loaded, the executor function is untouched, and pcov allocates no memory.
So enabling coverage is a mode switch, not an overlay. A single PHP process cannot give you both debugger-level tooling and pcov instrumentation. Teams that keep Xdebug loaded for local debugging can still keep it in a separate profile, but any CI job that depends on pcov output has to run without the other extensions present.
pcov.directory decides what gets recorded, and auto detection can waste the whole run
Configuration happens through PHP.ini. The option table lists pcov.enabled with a default of 1 and SYSTEM scope, pcov.directory defaulting to auto with SYSTEM and PERDIR scope, pcov.exclude defaulting to unused with the same scope, pcov.initial.memory defaulting to 65536, and pcov.initial.files defaulting to 64. The two scope levels matter in practice: a PERDIR value can be set per directory in a php.ini hierarchy, so a test suite can point the collector at its own tree.
The auto default has a sharp edge that the README spells out. When pcov.directory is left unset, PCOV will attempt to find src, lib or, app in the current working directory, in that order. If none are found the current directory will be used, which may waste resources storing coverage information for the test suite. In other words, an unconfigured checkout can silently instrument everything, including the tests that produce the coverage numbers.
The second lever is pcov.exclude, which excludes files under pcov.directory matching a PCRE. The guidance is conditional: if pcov.directory contains test code, it is recommended to set pcov.exclude to avoid wasting resources. The development defaults suggested are pcov.enabled = 1 and pcov.directory pointing at your source directory, while the recommended production default is pcov.enabled = 0.
Starting, stopping and collecting coverage from inside the test run
The API is a small set of functions in the pcov namespace. The signatures below are the ones the README documents, and they are the whole public surface:
function \pcov\start() : void;
function \pcov\stop() : void;
function \pcov\collect(int $type = \pcov\all, array $filter = []) : array;
function \pcov\clear(bool $files = false) : void;
function \pcov\waiting() : array;
function \pcov\memory() : int;Start and stop control recording. Collect gathers the data and returns an array, and its type argument selects the scope: \pcov\all collects coverage information for all files, \pcov\inclusive collects it for the specified files, and \pcov\exclusive collects it for all but the specified files. The filter parameter takes paths of files, resolved with realpath, that should be filtered. Clear discards stored information, and its files argument sets true to clear file tables, with a documented warning that clearing the file tables may have surprising consequences. Waiting returns the list of files waiting to be collected. Memory returns the current size of the trace and cfg arena, which is the number you need for sizing the arena before a large suite runs.
Arena chunking is why the memory defaults need adjusting for big suites
Two ini options govern allocation, and the README explains the arithmetic behind them. pcov.initial.memory sets the initial size of the arena and pcov.initial.files sets the initial size of tables. The advice is to set pcov.initial.memory according to the memory required by the test suite, which may be discovered with \pcov\memory(), and to set pcov.initial.files higher than the number of files that will be loaded during testing, inclusive of test files.
The mechanism is chunked allocation, and the note is worth reading carefully: arenas are allocated in chunks, so if the chunk size is set to 65536 and pcov requires 65537 bytes, the system will allocate two chunks, each 65536 bytes. The instruction that follows is to be generous in estimates when setting arena space. In other words, the defaults of 65536 and 64 are starting points, and a suite with many files or deep traces will reallocate.
The practical order of operations is therefore to run the suite once with pcov.enabled=1, read \pcov\memory() and count the loaded files, then set pcov.initial.memory and pcov.initial.files above those figures in php.ini before the run you actually intend to trust.
The reports are not identical to Xdebug, and the differences are structural
Both Xdebug and PCOV perform branch analysis in order to detect executable code, but they get there differently. Xdebug has custom written analysis described as mature and proven, while PCOV uses the control flow graph from the Optimizer extension. The README concludes that they generate comparably accurate reports, and then walks through where they still disagree.
The phpdbg case is the clearest example of why detection method matters. phpdbg uses weaker detection of executable code and generates reports with known defects. Given a function that returns true inside an if, phpdbg reports the function as 100% covered when the first control path is taken, because it cannot correctly detect which implicit return paths inserted by Zend at compile time are executable and so ignores them all. The README draws the conclusion from this that reports generated by phpdbg are not completely trustworthy.
For switch statements the two mature tools differ by a line. From PHP 7.2.15 and PCOV 1.0, PCOV detects the executability of the cases inside the switch body correctly, but will not detect the switch statement line itself as executable, because Zend did not output an executable opcode there. Xdebug uses a different method and shows an extra executable line. Before 7.2.15 and PCOV 1.0, coverage of some switches was questionable because of the way Zend constructs opcodes in the switch body, which may not execute depending on a jump table optimization. One capability gap remains open: Xdebug can produce path coverage and PCOV cannot, though the README notes path coverage is not yet implemented by CodeCoverage and probably will not be.
Where pcov is the wrong tool
Three cases are clearly wrong. If your reporting needs path coverage rather than line coverage, pcov cannot produce it. If your workflow needs Xdebug, phpdbg or the Blackfire probe in the same PHP process as the coverage run, the executor override makes that combination impossible. If you need a packaged binary install, the repository gives you a source build with INSTALL.md as the only pointer to requirements and steps.
Licensing deserves a separate look. The repository is listed with a licence of Other, described as a custom licence GitHub cannot classify, and a LICENSE file sits at the root. A badge that a licence classifier cannot read is not a signal about redistribution rights, so anyone shipping pcov in a product or a private vendor tree should read that file rather than infer terms from the repository metadata.
On upkeep, the last push was on 2026-07-21, with no GitHub releases published and work happening on the develop branch. That recency means the code is being touched, but there is no tagged release stream to pin against, so version management is left to whoever builds it.
Editorial conclusion
pcov suits teams running large PHP test suites in CI who want CodeCoverage output at low overhead and can live without Xdebug, phpdbg or Blackfire in the same run, and who are willing to read the LICENSE file, since GitHub cannot classify the licence. It does not suit anyone who needs path coverage, concurrent debug tooling, or a binary install without a source build. Before adopting it, check pcov.directory and pcov.exclude in your php.ini, run \pcov\memory() once to size pcov.initial.memory, and confirm your report consumer accepts the executable line differences that PCOV produces around switch statements.
Frequently asked questions
How do I install pcov for PHP?
The README's requirements and installation section points to INSTALL.md, and the repository root carries config.m4 and config.w32, so pcov is built as a PHP extension rather than installed from a binary package. The README does not list the individual build steps.
Can I use pcov together with Xdebug?
No. With pcov.enabled=1 the README states that interoperability with Xdebug, phpdbg and the Blackfire profiler is not possible, because pcov overrides the executor function. With pcov.enabled=0 Xdebug may be loaded and pcov is zero cost.
How do I limit pcov to my source directory?
Set pcov.directory in php.ini. If it is left unset, PCOV looks for src, lib or app in the current working directory in that order and falls back to the current directory, which can waste resources storing coverage information for the test suite.
Does pcov support path coverage?
No. The README notes that Xdebug is able to do path coverage while pcov does not, and that path coverage is not yet implemented by CodeCoverage and probably will not be.
How do I size memory for pcov?
pcov.initial.memory sets the initial arena size and defaults to 65536, while pcov.initial.files sets the initial table size and defaults to 64. Call \pcov\memory() during a run to learn the current size of the trace and cfg arena, then set pcov.initial.files above the number of files loaded during testing, inclusive of test files.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/krakjoe-pcov)Was this useful?