Zombie Hunt

Find and Safely Remove Dead Code

BEFORE WE BEGIN...

Slides/Code/Resources

Rate This Talk

joind.in/talk/3627e
wkdb.yt/zombie-hunt

Hello, World

wkdb.yt/zombie
  • Contract PHP Developer and Consultant
  • Currently Living in Dallas w/ Wife and Dog
  • Never Intended to Be a Developer

Three Laws of Software Development

wkdb.yt/zombie

1.  Change Happens

 

"Support and Maintenance" is a key part of the software development process. Delivering new features, updates, upgrades, bug fixes, and improvements to existing code is a fact of life for a PHP developer.

 

Three Laws of Software Development

wkdb.yt/zombie

2. Code Does Not Occur in Isolation

 

The constituent parts of an application, including third-party vendor code, make up a holistic system. Change to one part may require changes to other parts of the system.

 

Three Laws of Software Development

wkdb.yt/zombie

3. It is Cheaper to Remove than to Refactor

 

Code that does not exist, does not need to be maintained, nor does it accrue additional technical debt. In the case of third-party vendor code, this might mean "replace" or "upgrade".

 

It Was a Quiet Friday Afternoon...

wkdb.yt/zombie
wkdb.yt/zombie

It Was a Quiet Friday Afternoon...

wkdb.yt/zombie

It Was a Quiet Friday Afternoon...

wkdb.yt/zombie

It Was a Quiet Friday Afternoon...

wkdb.yt/zombie

Three Four Laws of Software Development

wkdb.yt/zombie

4. Nobody Is Happy When Production Breaks

 

You don't want to be the person who has to explain to the boss and/or customer that production went down because you deleted code that was otherwise working.

 

It could Happen to You!

wkdb.yt/zombie

It could Happen to You!

wkdb.yt/zombie
wkdb.yt/zombie

Introducing The Undead

Zombie Code:

Executable code that cannot reliably be identified as currently being live or dead, including both code you control and third-party vendor code.

wkdb.yt/zombie

Introducing The Undead

wkdb.yt/zombie
wkdb.yt/zombie

Maintainable

Removable

Zombie

The Zombie Problem

If the potential "zombie" is actually dead code, we may end up leaving it in the code base, reducing the maintainability of the code

If the potential "zombie" is actually live code, we may end up breaking production.

wkdb.yt/zombie

How To Hunt Zombies

wkdb.yt/zombie

First Thing First

wkdb.yt/zombie

Can This Code Even Run?:

If the code cannot possibly execute, e.g. there is a syntax or compile-time error in the current PHP runtime just remove it.

Preliminary Static Analysis

Try to find if an execution path exists for the code block in question.

wkdb.yt/zombie

Preliminary Static Analysis: GREP

wkdb.yt/zombie

Preliminary Static Analysis: PHPStorm

wkdb.yt/zombie

Preliminary Static Analysis: PHPStorm

"Unused Class/Method" Highlights

Manually Check Usages

wkdb.yt/zombie

Preliminary Static Analysis: PHPStorm

Inspect "Unused Declarations"

wkdb.yt/zombie

Static Analysis Pros/Cons

Pro: Fast to Run and Built Into IDE

Con: Prone to False Positive/Negative Results

wkdb.yt/zombie

Static Analysis of code can only reveal if an execution path to a block of code exists.

It cannot reveal if a block is being used.

Static Analysis Pros/Cons

wkdb.yt/zombie
wkdb.yt/zombie

What Is A TombStone?

A code tombstone is a production-safe mechanism that triggers when a block of code that we think is dead, is used in an active production environment.

We tag code that we believe to be dead, with a Tombstone, and wait to see if the Tombstone triggers.

If not, the code is dead, and we can safely delete it.

wkdb.yt/zombie

Let's Build a Tombstone!

wkdb.yt/zombie
<?php

namespace App\Service;

class Tombstone
{
    public static function exhume(): void
    {
        $backtrace = debug_backtrace();
        
        $zombie = $backtrace[1];
        
        $method = $zombie['class'] . $zombie['type'] . $zombie['function'];
        
        $tombstone = str_replace('\\', '_', $method);
        
        $graveyard = __DIR__ . 'path/to/app/graveyard/';

        if (!is_dir($graveyard) && !mkdir($graveyard, recursive: true) && !is_dir($graveyard)) {
            throw new \Exception('failed to create graveyard');
        }

        touch($graveyard . $tombstone);
    }
}

Debug Backtrace

wkdb.yt/zombie
wkdb.yt/zombie
<?php

namespace App\Service;

class Tombstone
{
    public static function exhume(): void
    {
        $backtrace = debug_backtrace();
        
        $zombie = $backtrace[1];
        
        $method = $zombie['class'] . $zombie['type'] . $zombie['function'];
        
        $tombstone = str_replace('\\', '_', $method);
        
        $graveyard =  __DIR__ . 'path/to/app/graveyard/';

        if (!is_dir($graveyard) && !mkdir($graveyard, 0644, true) && !is_dir($graveyard)) {
            throw new \Exception('failed to create graveyard');
        }

        touch($graveyard . $tombstone);
    }
}

We don't care who triggered the tombstone.

We don't care how many times the tombstone was triggered.

We only care that the tombstone was triggered at all.

We don't care why they triggered the tombstone.

wkdb.yt/zombie

BurYING the Zombie

class ZombieService
{
    public function getToken(): AccessToken
    {
        Tombstone::exhume();
        return new AccessToken([
            'access_token' => bin2hex(random_bytes(32)),
            'expires_in' => 3600,
        ]);
    }
}

We add the Tombstone to the suspect code...

wkdb.yt/zombie

Now, We Wait...

wkdb.yt/zombie

Checking The GraveYard

If the tombstone is triggered, a new file is created in our graveyard

It's alive!

wkdb.yt/zombie
<?php

namespace App\Console\Commands;

use App\Notifications\ZombiesFound;
use FilesystemIterator;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Notification;
use SplFileInfo;

class SendZombieNotificationEmail extends Command
{
    protected $signature = 'zombies:send-email';

    protected $description = 'Send an email listing any Zombies in the App Graveyard';

    public function handle(): void
    {
        $files = new FilesystemIterator(storage_path('app/graveyard'));
        
        $zombies = array_map(function (SplFileInfo $file) {
            return $file->getFilename();
        }, [...$files]);

        if (count($zombies)) {
            Notification::route('mail', 'developers@example.com')
                ->notify(new ZombiesFound($zombies));
        }
    }
}
wkdb.yt/zombie

Best Practices

If possible, if you want to integrate notifications for something like Slack or Bugsnag, do not trigger the notification in the Tombstone.

Create a worker that polls the graveyard and triggers the notifications.

 

wkdb.yt/zombie

Best Practices

If you do integrate to an external service, whether or not from inside the Tombstone,

 

YOU MUST IMPLEMENT SOME KIND OF FLOOD PREVENTION!

wkdb.yt/zombie

Best Practices

You must wait at least one reasonable "cycle" for your use case.

Tombstones are effective, not fast.

wkdb.yt/zombie

Best Practices

Just don't wait too long...

wkdb.yt/zombie

Advanced TombStones

wkdb.yt/zombie

Advanced TombStones

wickedbyte/tombstone

wkdb.yt/zombie

Advanced TombStones

Out of the Box:

  • ​PHP Error Handing
  • File System Graveyard
  • PSR-3 Loggers
  • PSR-6/PSR-16 Cache Implementations
  • PSR-14 Event Dispatcher Implementations
wkdb.yt/zombie

Advanced TombStones

wkdb.yt/zombie
use WickedByte\Tombstone\Graveyard;
use WickedByte\Tombstone\GraveyardConfiguration;
use WickedByte\Tombstone\Handlers\InMemoryRateLimitHandler;
use WickedByte\Tombstone\Handlers\PhpErrorHandler;

Graveyard::config(new GraveyardConfiguration(handlers: [
  new InMemoryRateLimitHandler(),
  new PhpErrorHandler(),
  // Add additional handlers for your project here...
]));

Advanced TombStones

wkdb.yt/zombie
class ZombieService
{
    public function getToken(): AccessToken
    {
        \tombstone('2024-01-01 This code is *probably* dead');
        return new AccessToken([
            'access_token' => bin2hex(random_bytes(32)),
            'expires_in' => 3600,
        ]);
    }
}

Advanced TombStones

wkdb.yt/zombie

Advanced TombStones

1. PSR-3 Compatible Logger

wkdb.yt/zombie

2. Reporting/Analysis Library

wkdb.yt/zombie
class ZombieService
{
    public function getToken(): AccessToken
    {
        tombstone('2019-10-24', 'andysnell');
        return new AccessToken([
            'access_token' => bin2hex(random_bytes(32)),
            'expires_in' => 3600,
        ]);
    }
}

Advanced TombStones

wkdb.yt/zombie

What About Our Vendor Code?

wkdb.yt/zombie

Dead Vendor Eviction

composer-unused/composer-unused

wkdb.yt/zombie

With a composer.json that looks like this...

wkdb.yt/zombie

Dead Vendor Eviction

We can run "composer unused"...

wkdb.yt/zombie

Dead Vendor Eviction

wkdb.yt/zombie

Dead Vendor Eviction

wkdb.yt/zombie

shipmonk/composer-dependency-analyser

Dead Vendor Eviction

wkdb.yt/zombie

Dead Vendor Eviction

Undead Prevention

wkdb.yt/zombie

Undead Prevention

Actively Remove Dead Code

The best time to remove dead code is when you are working on it.

 

Use your Version Control System

wkdb.yt/zombie

Undead Prevention

Don't Create Intentional Zombies!

Only commit living code.

(Even if you are really proud of the work)

wkdb.yt/zombie

Undead Prevention

Don't Use Third-Party Code Directly

Use wrapper and adapter classes to avoid using third-party classes directly.

wkdb.yt/zombie

Undead Prevention

Keep Dependencies Up to Date

Use your CI pipeline

Updates Should Be Part of Maintainance

wkdb.yt/zombie

Thank You!

Slides/Code/Resources

Rate This Talk

joind.in/talk/3627e
wkdb.yt/zombie-hunt