Wednesday, 17 July 2019

Trimming the Phat

Fig 1. A very fancy Tomb

We all think we know how dead code elimination works, we can just reference code coverage, or run static analysis, or rely on our own internal model of the code, which is always absolutely perfect ...

Dead can mean multiple things when we're talking about code, at least:
  • Compiler context - dead code is "unreachable", it can never be executed
  • Execution context - dead code is "unused", it has not been, or is not going to be called
The distinction between compile and execute in PHP is somewhat blurred, as a result, some dead code detection that should be part of the compiler have traditionally been part of code coverage. In the latest versions of PHP, opcache eliminates unreachable code.

Static analysis and coverage reports can tell you about dead code in those narrow scopes defined, but there is another sense in which code might be considered dead:
  • Code that is in fact unused in production
My manager recently asked me to come up with something so that we can detect dead code in this production sense.

I'm quite adept at bending PHP to my will, however, this task presents some not insignificant challenges. Normally, when we want to abuse PHP in some strange way, we're doing so in the name of testing. 

Testing is a nice quiet place, where there's only one process to care about, not much can go wrong. If you are careful, you can write some really nice tooling, the overhead is very acceptable, and people rave about it on twitter (pcov).

Production on the other hand is a scary place, where mistakes may cost a lot of money, where there are in the order of hundreds of processes to care about: Extracting statistical information from hundreds of processes without adversely affecting their performance is not a simple task.

Tombs

Tombs is my solution to the problem of detecting code that is unused in production. Code that even though may be reported as reachable, covered, or used, is in fact never called in production.

There's something quite pleasing about the requirements for a task translating almost perfectly into the solution. The requirements for Tombs were:
  • Must not use more CPU time in PHP processes than is absolutely necessary (i.e. be production ready)
  • Must report statistics centrally for every process in a pool
The first requirement, aside from the obvious, means that Tombs needs to have an API that faces the system rather than user land PHP, we can't inject code into production so the processes that gather statistics must be separate and might be on different machines entirely.

The second requirement means that Tombs needs to use shared mapped memory, like O+ or APC(u).

O+ and APC(u) both achieve safety in their use of shared memory by multiple processes using mutual exclusion - implemented either as file locks, pthread mutex, or the windows equivalent - this makes perfect sense for them. It means that even though many processes may compile the about to be cached file, or execute the function that returns the about to be cached variable, only one process can insert the file or variable into shared memory.

Reporting live statistics about a system is similar to trying to count the number of birds in flight over the earth - it will change while reporting. In this environment, mutex makes very little sense, what we need here is a lock free implementation of the structure that stores information we need to share. We need to know that no matter how large the set of data being returned, we don't have to exclude other processes from continuing to manipulate that data concurrently.

Using Tombs

Simply load Tombs in a production environment and without modifying any code, allow normal execution to take place over the course of hours, days, or weeks. 

Now, when you open the Tombs socket the data returned represents the functions and methods that have not been executed by any process in the pool since Tombs was started.

Using this data, you can now make decisions about the removal or refactoring of code to reduce or hopefully eliminate dead code.

If you use Tombs, reach out to me and tell me how it worked out for you ....

1 comment:

  1. Hello Joy
    Thank you for the idea and the extension!

    If I understood correctly, the extension populates the functions and marks them as used on every execute in the same pool.

    But if we talk about multiple processes/pools like a separate cli scripts or 2+ servers with php-fpm, maybe better return information only about executed functions or all populated ones with the flag which says it was called or not.
    Otherwise it will be impossible to understand what really is unused, especially in huge projects.

    For example, we have a class that contains 2 methods.
    Depending on the condition, we call only one of them. Thus, after each launch of the script in different pools, we will read from socket that another one is unused.
    Eventually we will merge all data received from all pools and get that both methods are not used, but in fact both are needed.

    ReplyDelete