Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
107 commits
Select commit Hold shift + click to select a range
e215aa3
Initial attempt, stuck on Windows
l0gicnz Feb 27, 2026
97f96ca
Second attempt, output line ending fix
l0gicnz Feb 27, 2026
346cc6a
First WSL attempt, many fixes...
l0gicnz Feb 28, 2026
8db9bb3
WSL attempt
l0gicnz Feb 28, 2026
73560fc
noob fix
l0gicnz Feb 28, 2026
382b7ab
Tweaks and changes, but probably slower.
l0gicnz Feb 28, 2026
ec82068
Hopefully faster!
l0gicnz Feb 28, 2026
f21fc05
different appraoch
l0gicnz Feb 28, 2026
cae2ed3
shaving more off?
l0gicnz Feb 28, 2026
4fbb930
worker and buffer tweak
l0gicnz Feb 28, 2026
df75440
Running out of ideas
l0gicnz Feb 28, 2026
b182eca
2.5883 on my machine, probably slower ona mac!
l0gicnz Feb 28, 2026
0deac3c
C-mon at least Top 10!
l0gicnz Feb 28, 2026
d9f14e5
One of these must be Top 10 worthy!
l0gicnz Feb 28, 2026
27fee2a
Maybe
l0gicnz Feb 28, 2026
b1ae3d5
Hopefully this'll run
l0gicnz Mar 1, 2026
0036413
Trying to optimise for hardware I don't have locally isn't easy. M1 s…
l0gicnz Mar 1, 2026
a40e514
A different approach, but probably slower.
l0gicnz Mar 1, 2026
eac9e7e
Fixed exception throw
l0gicnz Mar 1, 2026
f05f1c5
Fixed error handler
l0gicnz Mar 1, 2026
3f002f1
I want gold, so I deleted lines
l0gicnz Mar 1, 2026
11a0d93
I've been more careful, this shouldn't break the server. But hopefull…
l0gicnz Mar 2, 2026
5104be4
Date Tweak
l0gicnz Mar 2, 2026
248af70
I have added lines. And crossed fingers.
l0gicnz Mar 2, 2026
1c3a3a1
No Shmop!
l0gicnz Mar 2, 2026
12e2a18
Brain hurting
l0gicnz Mar 2, 2026
b59d864
Gone back in time, but added adjustments.
l0gicnz Mar 2, 2026
c601609
Didnt realise we were able to work outside of just Parser.php
l0gicnz Mar 3, 2026
83b3e42
Hold
l0gicnz Mar 3, 2026
2f86f66
Tweaky
l0gicnz Mar 3, 2026
0d866c4
Fixed..?
l0gicnz Mar 3, 2026
5096661
Last for one for now..
l0gicnz Mar 3, 2026
7fe6c7a
Not going to be a 2.5s run... But, hopefully fast for me.
l0gicnz Mar 4, 2026
909de79
dunno
l0gicnz Mar 6, 2026
ec49465
again
l0gicnz Mar 6, 2026
3e51df3
.
l0gicnz Mar 7, 2026
024633b
again
l0gicnz Mar 7, 2026
419bf81
again
l0gicnz Mar 7, 2026
254b952
again
l0gicnz Mar 7, 2026
4d36bcc
254b952
l0gicnz Mar 9, 2026
5684258
again
l0gicnz Mar 9, 2026
c9ebc69
again
l0gicnz Mar 9, 2026
39ecc48
again
l0gicnz Mar 9, 2026
b39b716
againnnnn
l0gicnz Mar 10, 2026
2d0f6cc
againnnnnmaybefixed
l0gicnz Mar 10, 2026
b018c60
lessbreaky
l0gicnz Mar 11, 2026
d1c5077
no idea here
l0gicnz Mar 11, 2026
75d81ef
2500
l0gicnz Mar 12, 2026
c0d9e10
2000
l0gicnz Mar 12, 2026
616c1d4
1750
l0gicnz Mar 12, 2026
5b317fb
1900
l0gicnz Mar 12, 2026
f1c73e2
again200
l0gicnz Mar 12, 2026
e070889
agaaaaiiin
l0gicnz Mar 12, 2026
4a4077d
again
l0gicnz Mar 12, 2026
bf7f261
again
l0gicnz Mar 12, 2026
589198e
again
l0gicnz Mar 13, 2026
1433d86
again
l0gicnz Mar 13, 2026
97d2b58
again
l0gicnz Mar 13, 2026
98959f9
again
l0gicnz Mar 13, 2026
9fb6c2b
again
l0gicnz Mar 13, 2026
043825f
older version
l0gicnz Mar 13, 2026
3d958be
again
l0gicnz Mar 13, 2026
a83d23e
is it much faster though
l0gicnz Mar 13, 2026
f390992
again with shmop
l0gicnz Mar 13, 2026
056322d
again no shmop, shmop bad
l0gicnz Mar 13, 2026
c26db7d
nine
l0gicnz Mar 13, 2026
1fcef53
ten
l0gicnz Mar 13, 2026
443f8b9
nine
l0gicnz Mar 13, 2026
75dc435
nine
l0gicnz Mar 13, 2026
088aff4
nine
l0gicnz Mar 13, 2026
52d5138
again
l0gicnz Mar 13, 2026
50fdf25
again
l0gicnz Mar 13, 2026
411a427
again
l0gicnz Mar 13, 2026
2d8db62
again
l0gicnz Mar 13, 2026
835fb6b
again
l0gicnz Mar 13, 2026
10bf12e
maybe
l0gicnz Mar 13, 2026
9daf21e
maybe
l0gicnz Mar 13, 2026
0016754
maybe
l0gicnz Mar 13, 2026
3fdc5cc
maybe
l0gicnz Mar 13, 2026
a3e2a18
maybe
l0gicnz Mar 13, 2026
6e00fe7
maybe
l0gicnz Mar 13, 2026
da1f2a9
maybe
l0gicnz Mar 14, 2026
7a56469
again
l0gicnz Mar 14, 2026
fc27820
again
l0gicnz Mar 14, 2026
a66d9d4
again
l0gicnz Mar 14, 2026
98799f7
again
l0gicnz Mar 14, 2026
b4f864e
again
l0gicnz Mar 15, 2026
a144b7c
again
l0gicnz Mar 15, 2026
7ab3e84
again
l0gicnz Mar 15, 2026
ecb36b0
again
l0gicnz Mar 15, 2026
63a90b3
again
l0gicnz Mar 15, 2026
3b7c544
again
l0gicnz Mar 15, 2026
a3bf2b2
again
l0gicnz Mar 15, 2026
65c11ba
again
l0gicnz Mar 15, 2026
643c03b
again
l0gicnz Mar 15, 2026
96cede3
again
l0gicnz Mar 15, 2026
0f5bbc8
should not break things
l0gicnz Mar 15, 2026
f49ccc1
should not break things
l0gicnz Mar 15, 2026
9a55437
should not break things again
l0gicnz Mar 15, 2026
52a6adf
again
l0gicnz Mar 15, 2026
4f8f3ef
again
l0gicnz Mar 15, 2026
29e3093
again
l0gicnz Mar 15, 2026
128cf99
again
l0gicnz Mar 15, 2026
9ae08c3
4core
l0gicnz Mar 15, 2026
59fedd2
nah8core
l0gicnz Mar 15, 2026
bd98fec
nah8core
l0gicnz Mar 15, 2026
c7b896a
Benching my best one last time. Ty guys, it's been fun!
l0gicnz Mar 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,16 @@ test-data-actual.json
.tempest
.env
.DS_Store
.benchmark
.benchmark
debug.php
test.php
parse.php
benchmark.sh
slug.sh
tests.sh
gere-*
mine_*
tempest:*
current_best.php
other.php
mtParse.php
327 changes: 324 additions & 3 deletions app/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,333 @@

namespace App;

use Exception;
use function array_fill;
use function chr;
use function chunk_split;
use function count;
use function fclose;
use function feof;
use function fgets;
use function fopen;
use function fread;
use function fseek;
use function ftell;
use function fwrite;
use function gc_disable;
use function pcntl_fork;
use function sodium_add;
use function str_repeat;
use function stream_select;
use function stream_set_chunk_size;
use function stream_set_read_buffer;
use function stream_set_write_buffer;
use function stream_socket_pair;
use function strlen;
use function strpos;
use function strrpos;
use function substr;
use function unpack;
use const SEEK_CUR;
use const SEEK_END;
use const STREAM_IPPROTO_IP;
use const STREAM_PF_UNIX;
use const STREAM_SOCK_STREAM;

final class Parser
{
public function parse(string $inputPath, string $outputPath): void
private const int WORKERS = 8;
private const int SLUG_TOTAL = 268;
private const int DATE_COUNT = 2191;
private const int INITIAL_READ = 181_000;
private const int DISC_READ = 1_048_576;
private const int CHUNK_READ = 163_840;
private const int UNROLL = 10;
private const CHUNK_GRAIN = 1 << 25;
private const string URL_PREF = 'https://stitcher.io/blog/';


public static function parse($inputPath, $outputPath)
{
gc_disable();

$dateIds = [];
$dates = [];
$dc = 0;
for ($y = 1; $y <= 6; $y++) {
for ($m = 1; $m <= 12; $m++) {
$maxD = match ($m) { 2 => $y === 4 ? 29 : 28, 4, 6, 9, 11 => 30, default => 31 };
$mStr = ($m < 10 ? '0' : '') . $m;
$ymStr = "{$y}-{$mStr}-";
for ($d = 1; $d <= $maxD; $d++) {
$dStr = ($d < 10 ? '0' : '') . $d;
$dateIds[$ymStr . $dStr] = $dc;
$dates[$dc] = '202' . $y . '-' . $mStr . '-' . $dStr;
$dc++;
}
}
}

$next = [];
for ($i = 255; $i > 0; $i--) { $next[chr($i - 1)] = chr($i); }

$fh = fopen($inputPath, 'rb');
stream_set_read_buffer($fh, 0);

$paths = [];
$seen = [];
$slugTotal = 0;
$raw = fread($fh, self::INITIAL_READ);
$lastNl = strrpos($raw, "\n");
if ($lastNl === false) { $lastNl = -1; }
$pos = 0;

while ($pos < $lastNl && $slugTotal < self::SLUG_TOTAL) {
$nl = strpos($raw, "\n", $pos + 52);
if ($nl === false || $nl > $lastNl) { break; }
$slug = substr($raw, $pos + 25, $nl - $pos - 51);
if (! isset($seen[$slug])) {
$paths[$slugTotal] = $slug;
$seen[$slug] = true;
$slugTotal++;
}
$pos = $nl + 1;
}

if ($slugTotal < self::SLUG_TOTAL) {
$carry = substr($raw, $lastNl + 1);
while ($slugTotal < self::SLUG_TOTAL && ! feof($fh)) {
$chunk = $carry . fread($fh, self::DISC_READ);
if ($chunk === '') { break; }
$lastNl = strrpos($chunk, "\n");
if ($lastNl === false) { $carry = $chunk; continue; }
$pos = 25;
while ($pos < $lastNl) {
$sep = strpos($chunk, ',', $pos);
if ($sep === false || $sep >= $lastNl) { break; }
$slug = substr($chunk, $pos, $sep - $pos);
if (! isset($seen[$slug])) {
$paths[$slugTotal] = $slug;
$seen[$slug] = true;
$slugTotal++;
if ($slugTotal === self::SLUG_TOTAL) { break 2; }
}
$pos = $sep + 52;
}
$carry = substr($chunk, $lastNl + 1);
}
}

fseek($fh, 0, SEEK_END);
$fileSize = ftell($fh);

$chunks = [];
$lo = 0;
while ($lo < $fileSize) {
$hi = $lo + self::CHUNK_GRAIN;
if ($hi > $fileSize) { $hi = $fileSize; }
$start = 0;
if ($lo > 0) { fseek($fh, $lo); fgets($fh); $start = ftell($fh); }
$end = $fileSize;
if ($hi < $fileSize) { fseek($fh, $hi); fgets($fh); $end = ftell($fh); }
$chunks[] = [$start, $end];
$lo = $hi;
}

fclose($fh);

$keyBytes = 1;
while (true) {
$keys = [];
foreach ($paths as $slug) {
$key = substr(self::URL_PREF . $slug, -$keyBytes);
if (isset($keys[$key])) { $keyBytes++; continue 2; }
$keys[$key] = true;
}
break;
}

$maxStride = 0;
$slugLookup = [];
foreach ($paths as $id => $slug) {
$stride = strlen($slug) + 52;
if ($stride > $maxStride) { $maxStride = $stride; }
$slugLookup[substr(self::URL_PREF . $slug, -$keyBytes)] = ($stride << 20) | ($id * $dc);
}

$bucketSize = $slugTotal * $dc;
$frameBytes = $bucketSize << 1;
$keyOffset = 26 + $keyBytes;
$slotMask = (1 << 20) - 1;
$batchLimit = ($maxStride * self::UNROLL) + $keyOffset;
$chunkCount = count($chunks);

$sockets = [];
for ($w = 0; $w < self::WORKERS; $w++) {
$pair = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);
stream_set_chunk_size($pair[0], $frameBytes);
stream_set_chunk_size($pair[1], $frameBytes);

if (pcntl_fork() === 0) {
fclose($pair[0]);
$output = str_repeat("\0", $bucketSize);
$handle = fopen($inputPath, 'rb');
stream_set_read_buffer($handle, 0);
$dateOff = 22;
$dateLen = 7;
$shift = 20;

for ($ci = $w; $ci < $chunkCount; $ci += self::WORKERS) {
[$start, $end] = $chunks[$ci];
fseek($handle, $start);
$remaining = $end - $start;

while ($remaining > 0) {
$toRead = $remaining > self::CHUNK_READ ? self::CHUNK_READ : $remaining;
$chunk = fread($handle, $toRead);
$chunkLen = strlen($chunk);
$remaining -= $chunkLen;
$lastNl = strrpos($chunk, "\n");
if ($lastNl === false) { break; }
$tail = $chunkLen - $lastNl - 1;
if ($tail > 0) { fseek($handle, -$tail, SEEK_CUR); $remaining += $tail; }

$pos = $lastNl;
while ($pos > $batchLimit) {
$token = $slugLookup[substr($chunk, $pos - $keyOffset, $keyBytes)];
$idx = ($token & $slotMask) + $dateIds[substr($chunk, $pos - $dateOff, $dateLen)];
$output[$idx] = $next[$output[$idx]];
$pos -= $token >> $shift;

$token = $slugLookup[substr($chunk, $pos - $keyOffset, $keyBytes)];
$idx = ($token & $slotMask) + $dateIds[substr($chunk, $pos - $dateOff, $dateLen)];
$output[$idx] = $next[$output[$idx]];
$pos -= $token >> $shift;

$token = $slugLookup[substr($chunk, $pos - $keyOffset, $keyBytes)];
$idx = ($token & $slotMask) + $dateIds[substr($chunk, $pos - $dateOff, $dateLen)];
$output[$idx] = $next[$output[$idx]];
$pos -= $token >> $shift;

$token = $slugLookup[substr($chunk, $pos - $keyOffset, $keyBytes)];
$idx = ($token & $slotMask) + $dateIds[substr($chunk, $pos - $dateOff, $dateLen)];
$output[$idx] = $next[$output[$idx]];
$pos -= $token >> $shift;

$token = $slugLookup[substr($chunk, $pos - $keyOffset, $keyBytes)];
$idx = ($token & $slotMask) + $dateIds[substr($chunk, $pos - $dateOff, $dateLen)];
$output[$idx] = $next[$output[$idx]];
$pos -= $token >> $shift;

$token = $slugLookup[substr($chunk, $pos - $keyOffset, $keyBytes)];
$idx = ($token & $slotMask) + $dateIds[substr($chunk, $pos - $dateOff, $dateLen)];
$output[$idx] = $next[$output[$idx]];
$pos -= $token >> $shift;

$token = $slugLookup[substr($chunk, $pos - $keyOffset, $keyBytes)];
$idx = ($token & $slotMask) + $dateIds[substr($chunk, $pos - $dateOff, $dateLen)];
$output[$idx] = $next[$output[$idx]];
$pos -= $token >> $shift;

$token = $slugLookup[substr($chunk, $pos - $keyOffset, $keyBytes)];
$idx = ($token & $slotMask) + $dateIds[substr($chunk, $pos - $dateOff, $dateLen)];
$output[$idx] = $next[$output[$idx]];
$pos -= $token >> $shift;

$token = $slugLookup[substr($chunk, $pos - $keyOffset, $keyBytes)];
$idx = ($token & $slotMask) + $dateIds[substr($chunk, $pos - $dateOff, $dateLen)];
$output[$idx] = $next[$output[$idx]];
$pos -= $token >> $shift;

$token = $slugLookup[substr($chunk, $pos - $keyOffset, $keyBytes)];
$idx = ($token & $slotMask) + $dateIds[substr($chunk, $pos - $dateOff, $dateLen)];
$output[$idx] = $next[$output[$idx]];
$pos -= $token >> $shift;
}

while ($pos >= $keyOffset) {
$token = $slugLookup[substr($chunk, $pos - $keyOffset, $keyBytes)];
$idx = ($token & $slotMask) + $dateIds[substr($chunk, $pos - $dateOff, $dateLen)];
$output[$idx] = $next[$output[$idx]];
$pos -= $token >> $shift;
}
}
}

fclose($handle);
fwrite($pair[1], chunk_split($output, 1, "\0"));
fclose($pair[1]);
exit(0);
}

fclose($pair[1]);
$sockets[$w] = $pair[0];
}

$buffers = array_fill(0, self::WORKERS, '');
$write = [];
$except = [];

while ($sockets !== []) {
$read = $sockets;
stream_select($read, $write, $except, 5);
foreach ($read as $id => $s) {
$data = fread($s, $frameBytes);
if ($data !== '' && $data !== false) { $buffers[$id] .= $data; }
if (feof($s)) { fclose($s); unset($sockets[$id]); }
}
}

$merged = $buffers[0];
for ($w = self::WORKERS - 1; $w > 0; $w--) { sodium_add($merged, $buffers[$w]); }

$counts = unpack('v*', $merged);
self::writeJson($outputPath, $counts, $paths, $dates, $dc, $slugTotal);
}

private static function writeJson($outputPath, $counts, $paths, $dates, $dateCount, $slugTotal)
{
throw new Exception('TODO');
$out = fopen($outputPath, 'wb');
stream_set_write_buffer($out, 1_048_576);
fwrite($out, '{');

$datePrefixes = [];
for ($d = $dateCount - 1; $d >= 0; $d--) {
$datePrefixes[$d] = ' "' . $dates[$d] . '": ';
}

$pathHeaders = [];
for ($p = $slugTotal - 1; $p >= 0; $p--) {
$pathHeaders[$p] = '"\/blog\/' . $paths[$p] . '": {';
}

$sep = "\n ";
$base = 1;

for ($p = 0; $p < $slugTotal; $p++) {
$firstDay = -1;
$c = $base;
for ($d = 0; $d < $dateCount; $d++) {
if ($counts[$c] !== 0) { $firstDay = $d; break; }
$c++;
}

if ($firstDay === -1) { $base += $dateCount; continue; }

$json = $sep . $pathHeaders[$p] . "\n" . $datePrefixes[$firstDay] . $counts[$c];
$sep = ",\n ";

for ($d = $firstDay + 1; $d < $dateCount; $d++) {
$c++;
if ($counts[$c] === 0) { continue; }
$json .= ",\n" . $datePrefixes[$d] . $counts[$c];
}

$json .= "\n }";
fwrite($out, $json);
$base += $dateCount;
}

fwrite($out, "\n}");
fclose($out);
}
}
7 changes: 6 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,10 @@
"prefer-stable": true,
"require-dev": {
"phpunit/phpunit": "^12.5"
},
"config": {
"optimize-autoloader": true,
"classmap-authoritative": true,
"apcu-autoloader": true
}
}
}
15 changes: 12 additions & 3 deletions tempest
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
#!/usr/bin/env php
<?php

use Tempest\Console\ConsoleApplication;
if (isset($argv[1]) && $argv[1] === 'data:parse') {
$input = $argv[2] ?? __DIR__ . '/data/data.csv';
$output = $argv[3] ?? __DIR__ . '/data/data.json';
if (($p = strpos($input, '=')) !== false) $input = substr($input, $p + 1);
if (($p = strpos($output, '=')) !== false) $output = substr($output, $p + 1);

require_once getcwd() . '/vendor/autoload.php';
require_once __DIR__ . '/app/Parser.php';
App\Parser::parse($input, $output);
exit(0);
}

ConsoleApplication::boot(loadBuiltInCommands: false)->run();
require_once __DIR__ . '/vendor/autoload.php';

\Tempest\Console\ConsoleApplication::boot(loadBuiltInCommands: false)->run();

exit;