This commit is contained in:
2026-11-02 20:29:34 +02:00
parent 340f3bb5fa
commit fcf31e8d48
181 changed files with 33383 additions and 51 deletions
@@ -0,0 +1,185 @@
<?php
namespace Tests\Benchmarks;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use App\Modules\Identity\Http\ModuleRecordsController;
use App\Modules\Identity\Services\PreProjectionModuleAssociationReview;
require_once __DIR__.'/ModuleAssociationLoadTest.php';
final class PreProjectionAssociationController
{
public function index(Request $r) { return response()->json(app(PreProjectionModuleAssociationReview::class)->listing($r))->header('Cache-Control', 'no-store'); }
}
final class ModuleAssociationFanoutTest extends ModuleAssociationLoadTest
{
private ?array $metrics = null;
private ?array $plan = null;
private int $start = 0;
public function test_synthetic_volume_and_query_plans(): void
{
$run = (string) getenv('SGF_ISOLATED_REGRESSION_RUN');
$this->assertMatchesRegularExpression('/^[a-f0-9]{12}$/D', $run);
$identity = DB::selectOne("SELECT current_database() AS database,current_user AS role,inet_server_port() AS port,current_setting('cluster_name') AS cluster");
$this->assertSame('sgf_tempo_test', $identity->database); $this->assertSame('sgf_test_runner', $identity->role);
$this->assertSame('sgf_modular_regression_'.$run, $identity->cluster); $this->assertSame(55439, (int) $identity->port);
$this->assertSame('8.4.27', PHP_VERSION);
require_once base_path('../pre-projection-review.php');
$this->withoutExceptionHandling();
$before = $this->fingerprints();
$report = ['date' => '2026-11-02', 'identity' => $identity, 'optimized_sha256' => hash_file('sha256', base_path('app/Modules/Identity/Services/ModuleAssociationReview.php')),
'pre_projection_sha256' => trim(file_get_contents(base_path('../pre-projection-sha256.txt'))),
'php_memory_limit' => ini_get('memory_limit'), 'records_per_area' => 10000, 'lines_per_draft' => 100,
'bounds' => ['request_ms' => 15000, 'statement_ms' => 3000, 'queries' => 250, 'php_used_bytes' => 201326592],
'method' => 'New synthetic fixtures, three sequential warm in-process HTTP samples; same profile across no-index/revision-only/pair phases; no product changes or deployment capacity claim.', 'profiles' => []];
DB::listen(function (QueryExecuted $q) {
if ($this->metrics === null) return;
if (!preg_match('/^\s*(select|with)\b/i', $q->sql)) throw new \RuntimeException('READ_WORKLOAD_WROTE_SQL');
$this->metrics['queries']++; $this->metrics['database_ms'] += $q->time;
if (str_contains($q->sql, 'identity_audit') && ($this->plan === null || str_contains($q->sql, 'from "a_clients"'))) $this->plan = ['sql' => $q->sql, 'bindings' => $q->bindings];
if ((hrtime(true) - $this->start) / 1e6 > 15000 || $this->metrics['queries'] > 250 || memory_get_usage(false) > 201326592) throw new \RuntimeException('DIAGNOSTIC_BUDGET_REACHED');
});
DB::beginTransaction();
try {
$f = $this->fixture(10000, true); $actor = $f['identity'];
$this->actingAs($actor['manager'])->withSession(['auth_version' => $actor['manager']->auth_version]);
$token = $this->postJson('/api/context', ['tenant_id' => $actor['tenant'], 'company_id' => $actor['company'], 'unit_id' => $actor['unit'], 'module_id' => null])->assertOk()->json('context.token');
$this->withHeader('X-SGF-Context', $token);
foreach ([128, 1000] as $descriptionBytes) {
$version = $this->expand($f, $descriptionBytes);
$profile = ['line_description_bytes' => $descriptionBytes, 'draft_version' => $version, 'variants' => []];
$cases = [];
foreach (['service_drafts', 'billing_drafts'] as $area) foreach ([25, 100] as $size) $cases[] = [$area, 'all', $size];
if ($descriptionBytes === 128) foreach (['pending', 'associated', 'removed'] as $state) $cases[] = ['clients', $state, 25];
$hashes = [];
foreach (['pre_projection', 'none', 'revision_only', 'pair'] as $mode) {
$this->controller($mode);
DB::beginTransaction();
try {
$ddl = [];
if (in_array($mode, ['revision_only', 'pair'], true)) $ddl[] = "CREATE INDEX sgf_qa_assoc_revision ON identity_audit (tenant_id,company_id,(details->>'area'),(details->>'record_id'),id DESC) WHERE action IN ('module.record.associated','module.record.disassociated','module.record.reviewed')";
if ($mode === 'pair') $ddl[] = "CREATE INDEX sgf_qa_assoc_links ON identity_audit (tenant_id,company_id,(details->>'area'),(details->>'record_id'),(details->>'module_id'),id DESC) WHERE action IN ('module.record.associated','module.record.disassociated')";
$start = hrtime(true);
foreach ($ddl as $sql) DB::statement($sql);
if ($ddl) DB::statement('ANALYZE identity_audit');
$variant = ['ddl' => $ddl, 'index_build_and_analyze_ms' => round((hrtime(true) - $start) / 1e6, 3), 'index_bytes' => [], 'cases' => []];
foreach (['sgf_qa_assoc_revision', 'sgf_qa_assoc_links'] as $name) {
$bytes = DB::selectOne('SELECT pg_relation_size(to_regclass(?)) AS bytes', [$name])->bytes;
if ($bytes !== null) $variant['index_bytes'][$name] = (int) $bytes;
}
foreach ($cases as [$area, $state, $size]) {
$expected = array_values(array_filter($f['ids'][$area], fn ($id) => match ($state) { 'pending' => $id % 3 === 0, 'associated' => $id % 3 === 1, 'removed' => $id % 3 === 2, default => true }));
$this->getJson('/api/module-records?area='.$area.'&per_page=1')->assertOk();
$case = ['area' => $area, 'state' => $state, 'page_size' => $size, 'samples' => []];
for ($i = 0; $i < 3; $i++) {
$sample = $this->sample($f, $area, $state, $size, $expected, $version);
$case['samples'][] = $sample;
if ($sample['status'] !== 'ok') break;
$hashes["$area/$state/$size"][] = $sample['response_sha256'];
}
$ok = array_values(array_filter($case['samples'], fn ($s) => $s['status'] === 'ok'));
$times = array_column($ok, 'elapsed_ms'); sort($times);
$case['median_ms'] = count($ok) === 3 ? $times[1] : null;
$case['complete'] = count($ok) === 3;
$case['planned_indexes'] = $this->plannedIndexes();
$variant['cases'][] = $case;
fwrite(STDOUT, "\nFANOUT $descriptionBytes $mode $area/$state/$size ".($case['median_ms'] ?? 'bounded')."ms\n");
}
$profile['variants'][$mode] = $variant;
} finally { $this->metrics = null; DB::rollBack(); }
$this->assertSame(0, (int) DB::selectOne("SELECT count(*) AS n FROM pg_indexes WHERE schemaname='public' AND indexname LIKE 'sgf_qa_assoc_%'")->n);
}
foreach ($hashes as $values) $this->assertCount(1, array_unique($values), 'All complete index variants must preserve the same response');
$profile['complete_responses_identical'] = true;
$report['profiles'][] = $profile;
}
} finally {
$this->metrics = null; DB::rollBack();
$report['original_test_data_and_indexes_preserved'] = $before === $this->fingerprints();
$output = getenv('SGF_ASSOCIATION_LOAD_OUTPUT');
if (!$output || realpath(dirname($output)) !== realpath(base_path('..'))) throw new \RuntimeException('Unexpected report directory');
file_put_contents($output, json_encode($report, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR).PHP_EOL);
}
$this->assertTrue($report['original_test_data_and_indexes_preserved']);
}
private function controller(string $mode): void
{
app()->bind(ModuleRecordsController::class, fn () => $mode === 'pre_projection' ? new PreProjectionAssociationController() : new ModuleRecordsController());
foreach (app('router')->getRoutes() as $route) if ($route->uri() === 'api/module-records') $route->flushController();
}
private function expand(array $f, int $descriptionBytes): int
{
$versions = DB::table('a_service_drafts')->whereIn('id', array_slice($f['ids']['service_drafts'], 0, 100))->pluck('version', 'id');
$version = (int) $versions[$f['ids']['service_drafts'][0]] + 1;
for ($i = 0; $i < 100; $i++) {
$lines = [];
foreach (array_slice($f['ids']['services'], $i * 100, 100) as $id) $lines[] = ['service_id' => $id, 'quantity' => '1', 'unit_price' => '10', 'description' => str_pad('SYNTHETIC-WIDE-', $descriptionBytes, 'x')];
$data = ['currency' => 'MZN', 'tax_status' => 'pending', 'accounting_status' => 'not_processed', 'lines' => $lines];
DB::table('a_service_drafts')->where('id', $f['ids']['service_drafts'][$i])->update(['version' => $version, 'data' => json_encode($data, JSON_THROW_ON_ERROR), 'updated_at' => now()]);
$data['source_version'] = $version;
DB::table('a_billing_drafts')->where('id', $f['ids']['billing_drafts'][$i])->update(['version' => $version, 'data' => json_encode($data, JSON_THROW_ON_ERROR), 'updated_at' => now()]);
}
foreach (['a_service_drafts', 'a_billing_drafts'] as $table) DB::statement('ANALYZE '.$table);
return $version;
}
private function sample(array $f, string $area, string $state, int $size, array $expected, int $version): array
{
DB::beginTransaction(); DB::statement("SET LOCAL statement_timeout = '3s'"); gc_collect_cycles();
$this->plan = null; $this->metrics = ['queries' => 0, 'database_ms' => 0.0];
memory_reset_peak_usage(); $base = memory_get_usage(false); $this->start = hrtime(true); $extra = []; $status = 'ok';
try {
$r = $this->getJson('/api/module-records?'.http_build_query(['area' => $area, 'state' => $state, 'per_page' => $size]));
$elapsed = (hrtime(true) - $this->start) / 1e6; $stats = $this->metrics; $this->metrics = null;
$peak = memory_get_peak_usage(false); $allocated = memory_get_peak_usage(true);
$r->assertOk()->assertJsonPath('pagination.total', count($expected));
$rows = $r->json('records.'.$area); $this->assertSame(array_slice($expected, 0, $size), array_column($rows, 'id'));
$positions = array_flip(array_slice($f['ids'][$area], 0, 100));
foreach ($rows as $row) {
$this->assertFalse($row['dependencies_restricted']);
if ($area === 'clients') continue;
$this->assertSame($version, $row['version']);
$this->assertCount($area === 'billing_drafts' ? 102 : 101, $row['dependencies']);
$serviceIds = array_column(array_values(array_filter($row['dependencies'], fn ($d) => $d['area'] === 'services')), 'id');
$this->assertSame(array_slice($f['ids']['services'], $positions[$row['id']] * 100, 100), $serviceIds);
}
$this->assertStringNotContainsString('SYNTHETIC-WIDE-', $r->getContent(), 'Document contents must not leak into administrative metadata');
$extra = ['rows' => count($rows), 'response_bytes' => strlen($r->getContent()), 'response_sha256' => hash('sha256', $r->getContent())];
} catch (\Illuminate\Database\QueryException $e) { $status = 'sql_timeout_or_error'; $extra = ['sqlstate' => $e->errorInfo[0] ?? $e->getCode()];
} catch (\RuntimeException $e) { if ($e->getMessage() !== 'DIAGNOSTIC_BUDGET_REACHED') throw $e; $status = 'diagnostic_budget_reached';
} finally {
$elapsed ??= (hrtime(true) - $this->start) / 1e6; $stats ??= $this->metrics; $peak ??= memory_get_peak_usage(false); $allocated ??= memory_get_peak_usage(true);
$this->metrics = null; DB::rollBack();
}
return ['status' => $status, 'elapsed_ms' => round($elapsed, 3), 'queries' => $stats['queries'], 'database_ms' => round($stats['database_ms'], 3),
'incremental_php_used_bytes' => max(0, $peak - $base), 'peak_php_used_bytes' => $peak, 'peak_php_allocated_bytes' => $allocated, ...$extra];
}
private function plannedIndexes(): array
{
if (!$this->plan) return [];
DB::beginTransaction(); DB::statement("SET LOCAL statement_timeout = '3s'");
try {
$raw = DB::select('EXPLAIN (FORMAT JSON) '.$this->plan['sql'], $this->plan['bindings']);
$plan = json_decode($raw[0]->{'QUERY PLAN'}, true, 512, JSON_THROW_ON_ERROR)[0]['Plan']; $names = [];
$visit = function (array $node) use (&$visit, &$names): void { if (isset($node['Index Name'])) $names[] = $node['Index Name']; foreach ($node['Plans'] ?? [] as $child) $visit($child); };
$visit($plan); $visit = null;
return array_values(array_unique($names));
} finally { DB::rollBack(); }
}
private function fingerprints(): array
{
$out = [];
foreach (['a_clients', 'a_services', 'a_service_drafts', 'a_billing_drafts', 'identity_audit', 'users', 'permission_grants', 'company_memberships'] as $table) $out[$table] = (array) DB::selectOne("SELECT count(*) AS rows,md5(coalesce(string_agg(row_to_json(t)::text,E'\\n' ORDER BY row_to_json(t)::text),'')) AS hash FROM {$table} t");
$out['indexes'] = json_encode(DB::select("SELECT indexname,indexdef FROM pg_indexes WHERE schemaname='public' ORDER BY indexname"), JSON_THROW_ON_ERROR);
return $out;
}
}