Passa al contenuto principale

Inventario HW dei device

device_hw_snapshots è il catalogo hardware per-device costruito a partire dagli artefatti di registrazione. Una riga per ogni (user_id, hw_fingerprint) distinta. Le sessioni si collegano a uno snapshot tramite recording_sessions.hw_snapshot_id, così i problemi di qualità si possono correlare con le specs hardware senza uscire da Supabase.

Background: questo sistema ship EC-20. Lo stato precedente teneva l'hardware dentro bug_reports.hw_config (sparso — solo gli utenti che fanno report) o solo su S3 dentro ogni _meta.json (non interrogabile da Supabase). Nessuno dei due bastava per analytics di coorte tipo "tutte le sessioni CS2 fallite sul branch driver NVIDIA X".

Pipeline

Lo stesso payload hardware appare in tre posti — *_meta.json::hardware su S3, device_hw_snapshots.snapshot su Supabase e bug_reports.hw_config — perché tutti originano da un unico producer: collectSafeRecordingHardwareMetadata() in playroll-core.

Schema canonico

Lo schema del payload (da playroll-core::recording_metadata_enricher.cpp):

{
"privacy": {
"hostname": "excluded",
"username": "excluded",
"serial_numbers": "excluded",
"mac_addresses": "excluded",
"adapter_luid": "excluded",
"device_instance_paths": "excluded"
},
"system": {
"platform": "windows",
"cpu_architecture": "x64",
"logical_processors": 20,
"page_size_bytes": 4096,
"cpu_model": "Intel(R) Core(TM) Ultra 7 255HX",
"os_release": "10.0.26200",
"physical_memory_bytes": 33738285056,
"physical_memory_mib": 32175
},
"gpus": [
{
"index": 0,
"description": "NVIDIA GeForce RTX 5070 Ti Laptop GPU",
"vendor_name": "nvidia",
"vendor_id": 4318,
"device_id": 12056,
"dedicated_video_memory_bytes": 12884901888,
"dedicated_video_memory_mib": 12288,
"dedicated_system_memory_bytes": 0,
"shared_system_memory_bytes": 16869142528,
"output_count": 1,
"is_software_adapter": false,
"driver_version": "32.0.15.9597",
"driver_version_nvidia": "595.97"
}
]
}
CampoNote
privacyBlocco di audit: quali identificatori sensibili sono stati intenzionalmente esclusi. Hostname, username Windows, MAC, serial hardware, LUID DXGI e device instance paths non vengono mai raccolti — vedi collectSafeRecordingHardwareMetadata().
system.cpu_modelStringa di brand CPUID (leaves 0x80000002..04).
system.os_releaseBuild Windows via RtlGetVersion (sopravvive ai compatibility shim).
gpus[].vendor_nameNormalizzato: nvidia, amd, intel, microsoft, other.
gpus[].vendor_id / device_idInteri DXGI raw — utili per joinare contro cataloghi GPU esterni.
gpus[].driver_versionForma Windows raw, es. 32.0.15.9597.
gpus[].driver_version_nvidiaForma NVIDIA XXX.YY. Popolato solo per adapter NVIDIA (vendor_id = 0x10DE). Formula: last5 = (build_word % 10)*10000 + rev_word%10000, poi major = last5/100, minor = last5%100.

Tabella

create table public.device_hw_snapshots (
id uuid primary key,
created_at timestamptz,
updated_at timestamptz,
user_id text not null,
hw_fingerprint text not null,
-- Colonne denormalizzate per quick-filter:
cpu_model text,
cpu_architecture text,
logical_processors integer,
physical_memory_mib bigint,
os_release text,
primary_gpu_vendor text,
primary_gpu_name text,
primary_gpu_driver text,
primary_gpu_driver_nvidia text,
-- Payload completo, verbatim:
snapshot jsonb,
source text default 'core_meta_json',
constraint device_hw_snapshots_unique unique (user_id, hw_fingerprint)
);

Le colonne denormalizzate riflettono i campi più utili dentro snapshot per permettere alle dashboard di filtrare e raggruppare senza operatori jsonb. Il payload completo resta disponibile in snapshot per qualsiasi analisi che necessiti della long tail (es. macchine multi-GPU: solo la primary è denormalizzata, il resto resta nell'array).

recording_sessions.hw_snapshot_id è la FK nullable da sessione → snapshot, con ON DELETE SET NULL così una purge degli snapshot non perde le sessioni stesse.

Selezione GPU primaria

Quando una macchina ha più adapter (molto comune sui laptop: NVIDIA discrete + Intel iGPU + Microsoft Basic), le colonne denormalizzate primary_gpu_* descrivono l'adapter usato per encoding, non l'adapter di boot o quello di display. Ordine di selezione in on_upload:

  1. Primo adapter NVIDIA discrete (vendor_id == 0x10DE e dedicated_video_memory_bytes > 0).
  2. Altrimenti, adapter con il valore più alto di dedicated_video_memory_bytes.
  3. Altrimenti null.

Questo combacia con ciò che la pipeline pure-v2 NVENC effettivamente usa per l'encoding. L'array gpus[] completo resta in snapshot per i casi in cui serva.

Fingerprint

hw_fingerprint = sha256(canonical_fields). I canonical fields e il formato usato per costruire il blob (verbatim da lambda_src/on_upload/handler.py::_compute_hw_fingerprint):

cpu_architecture=<v>|cpu_model=<v>|logical_processors=<v>|physical_memory_mib=<v>|os_release=<v>|gpu_vendor=<v>|gpu_name=<v>|gpu_driver=<v>

Proprietà:

  • Stabile tra le registrazioni sullo stesso device — la stessa macchina produce la stessa fingerprint per sempre, quindi 100 sessioni si deduplicano a 1 riga + 100 FK.
  • Si divide naturalmente sui cambiamenti real-world che vuoi vedere come confine di coorte: aggiornamento driver, upgrade RAM, nuova GPU, feature update dell'OS.
  • Privacy-safe: nessuno degli input è PII. La fingerprint non include LUID, MAC, serial hardware o hostname.
  • Non committing crittograficamente: è una dedup key, non una prova d'identità. Un attaccante con lo stesso HW potrebbe collidere intenzionalmente, e va bene — il campo è per inventario, non per autenticazione.

Disaccoppiamento dei lifecycle

device_hw_snapshots viene popolata da on_upload quando il sidecar *_meta.json arriva su S3. Questo succede dopo che la registrazione è terminata e dopo che gli artefatti mp4 e csv sono stati caricati. La riga recording_sessions, invece, viene creata in anticipo da auth_presigned::handle_session_started quando la registrazione inizia.

Questi due lifecycle sono intenzionalmente indipendenti:

  • La riga sessione esiste molto prima che lo snapshot arrivi, quindi il linking è un semplice UPDATE recording_sessions SET hw_snapshot_id = ... WHERE session_id = ....
  • Se il *_meta.json non arriva mai (es. registrazione abortita prima del finalize), la riga sessione resta con hw_snapshot_id IS NULL e quella è la semantica corretta — non c'è snapshot canonico per quella sessione.
  • Se l'utente cambia hardware tra registrazioni, il _meta.json successivo produce una nuova fingerprint → nuova riga device_hw_snapshots → le sessioni successive si linkano alla nuova riga. I confronti before/after di coorte vengono fuori gratuitamente.

RLS

alter table public.device_hw_snapshots enable row level security;

-- Gli utenti vedono solo i propri snapshot:
create policy device_hw_snapshots_select_own
on public.device_hw_snapshots for select to authenticated
using (user_id = (auth.uid())::text);

-- Tutte le write passano per la Lambda service-role — nessuna mutazione client:
create policy device_hw_snapshots_insert_service_only ...
create policy device_hw_snapshots_update_service_only ...
create policy device_hw_snapshots_delete_service_only ...

La Lambda si autentica con SUPABASE_SERVICE_ROLE_KEY. Gli utenti hanno accesso read-only alle proprie righe (utile per una futura UI "i tuoi device"). Gli operatori interrogano via dashboard con la service role.

Pattern di query

Sessioni su uno specifico branch driver NVIDIA

select rs.session_id, rs.game_slug, rs.uploadability_status,
d.primary_gpu_name, d.primary_gpu_driver_nvidia
from public.recording_sessions rs
join public.device_hw_snapshots d on d.id = rs.hw_snapshot_id
where d.primary_gpu_driver_nvidia like '560.%'
and rs.recording_started_at > now() - interval '7 days';

Distribuzione dei branch driver NVIDIA nel pilot

select primary_gpu_driver_nvidia,
count(distinct user_id) as users,
count(*) as devices
from public.device_hw_snapshots
where primary_gpu_driver_nvidia is not null
group by primary_gpu_driver_nvidia
order by users desc;

Utenti il cui driver è sotto la soglia minima dell'NVENC SDK

La soglia è loggata dal core all'avvio come service.startup_context_flags.nvenc_sdk_min_driver_version. Per confrontare in SQL bisogna parsare le stringhe XXX.YY:

with floor as (select '570.00'::numeric as v)
select user_id, primary_gpu_driver_nvidia
from public.device_hw_snapshots, floor
where primary_gpu_driver_nvidia is not null
and primary_gpu_driver_nvidia::numeric < floor.v;

(La soglia attuale è 570.00 — vedi Core Signals → Campi di contesto allo startup.)

Utenti multi-device

select user_id, count(*) as device_count
from public.device_hw_snapshots
group by user_id having count(*) > 1
order by device_count desc;

Questa è anche la shape dei dati necessaria per future gating device-specifici (guard di redemption key/reward, check multi-device). (user_id, hw_fingerprint) è l'identificatore preciso — lo stesso utente da una macchina diversa è una hw_fingerprint diversa.

Note operative

  • Popolazione best-effort. La Lambda on_upload tratta ogni step (fetch, parse, upsert, link) come non-fatal. I fallimenti loggano via OTel (hw_snapshot.linked, hw_snapshot.upsert_failed, hw_snapshot.meta_unreadable) ma non rompono mai la pipeline di upload. Le sessioni il cui meta fallisce il parsing semplicemente restano con hw_snapshot_id = null.
  • Cap di dimensione sidecar. Il _meta.json è cappato a 256 KiB dalla Lambda per difendersi da un upload spropositato malevolo. I sidecar reali sono pochi KiB.
  • Backfill. Le sessioni pre-deploy (hw_snapshot_id IS NULL) non sono popolate automaticamente. Uno script one-shot che cammina su s3_uploads WHERE filename LIKE '%_meta.json' sarebbe semplice da scrivere se servisse; è intenzionalmente out of scope al 2026-05-17.
  • Producer label. source = 'core_meta_json' è l'unico producer attuale. La colonna è riservata per produttori futuri (es. uno snapshot al momento dell'onboarding via GET /api/hw/snapshot direttamente in una write Supabase).

Riferimenti