george-andrikopoulos/domain-low-latency

Discipline d'ingénierie pour travailler en environnement de faible latence. Couvre : attaquer la conception lors d'une seconde passe ; une mesure correspond au régime qu'elle rapporte ; une file sans limite n'a pas de comportement en surcharge ; une structure conserve le régime sous lequel elle a été prouvée ; alloué n'est pas résident ; répondre à l'exigence à sa couche ; pas d'allocation sur le chemin critique ; pas d'omission coordonnée ; pas de faux partage sur une ligne chaude ; pas de boucle de nouvelle tentative sur un chemin contesté ; pas de blocage dans une fenêtre de publication ; pas d'appel système sur un chemin borné ;...

Qu'est-ce que domain-low-latency ?

Ce skill définit une discipline d'ingénierie rigoureuse pour les systèmes à faible latence, destinée principalement aux développeurs Rust et Java. Il énonce une série de règles essentielles, allant de la mesure correcte des performances (en précisant le régime thermique) à la gestion des files d'attente bornées, en passant par l'intégrité des structures de données sous des régimes de concurrence spécifiques. Chaque règle est accompagnée d'explications détaillées, de modes de défaillance concrets et de contre-mesures pratiques. Le skill est conçu comme un guide opérationnel pour les équipes techniques (par exemple, utilisant Claude Code, Cursor ou Codex) afin de prévenir les erreurs de conception coûteuses dans les composants critiques en termes de latence. Il met l'accent sur la vérification des hypothèses, l'utilisation de la sécurité de type (Rust) ou d'autres mécanismes (Java) pour rendre les états invalides non représentables, et la distinction entre mémoire allouée et résidente.

Compatible avec~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/george-andrikopoulos/relearn/tree/HEAD/claude-pack/skills/domain-low-latency

Installed? Explore more Développement et programmation skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

domaine : règles de faible latence

Une mesure indique si elle a été réalisée à chaud ou à froid, et correspond à la production [R:une-mesure-correspond-au-régime-qu'elle-rapporte]

Écrit pour les publics Rust et Java.

Écrit à partir de JMH et de ses exemples (Aleksey Shipilev), dont l'existence est l'affirmation qu'un microbenchmark fait à la main sur la JVM est erroné par défaut.

Indiquez dans quel régime la mesure a été effectuée et vérifiez qu'il s'agit du régime dans lequel la production s'exécute. Deux erreurs se cachent ici, et une seule d'entre elles a une réputation.

Mesurer à froid alors que la production est à chaud. Une première exécution se déroule en interprété ou depuis un cache d'instructions froid, avec un prédicteur de branche non entraîné, des tables de pages non peuplées et, sur un runtime managé, aucun code compilé. Le chiffre peut être un à deux ordres de grandeur pire que l'état stable, et il décrit l'échauffement plutôt que le travail. Sur la JVM, l'escalade est progressive -- interpréteur, puis C1, puis C2, avec remplacement sur la pile pour les boucles longues -- et prend des milliers d'itérations, c'est pourquoi une boucle de chronométrage faite à la main est erronée par défaut et pourquoi un harnais existe.

Mesurer à chaud alors que la production est à froid, ce qui est la moitié qui ne reçoit aucune attention car elle ressemble à de la rigueur. Un chemin qui s'exécute une fois par minute n'est jamais chaud. Une rafale d'ouverture de marché arrive dans du code froid. Un gestionnaire d'erreur s'exécute pour la première fois de la journée au pire moment possible. Pour chacun de ceux-ci, le chiffre à chaud est un nombre qu'aucun utilisateur ne verra jamais, et l'échauffement qui a été soigneusement écarté est le comportement de production. L'écarter n'est pas une hygiène là-bas ; c'est supprimer la mesure.

La règle est donc une correspondance de portée plutôt qu'une procédure : dites quel régime a été mesuré, dites dans quel régime se trouve la production, et montrez qu'ils sont identiques. Là où la production est véritablement les deux -- un flux régulier avec des rafales à froid -- cela donne deux mesures et deux chiffres rapportés, pas un seul moyenné jusqu'à l'insignifiance.

C'est [R:mesurer-la-prétention-pas-un-sous-ensemble] appliqué au temps plutôt qu'à un ensemble : les échantillons d'échauffement sont un sous-ensemble, les écarter est une décision de portée, et une décision de portée prise par réflexe est celle qui fabrique un mauvais nombre avec l'autorité d'avoir été mesuré.

Vérification du mode de défaillance : le chemin de production est-il à chaud ou à froid au moment où cela compte, et qu'ai-je mesuré ? Si le benchmark a écarté les N premières itérations, nommez ce que la production a qui correspond à ces itérations. Si rien ne le fait, la partie écartée était la réponse.

Deux voisins, délibérément séparés. [R:pas-d-omission-coordonnée] est l'instrument qui ne prend pas d'échantillons pendant les événements qui comptent ; celui-ci est l'instrument qui les prend dans le mauvais régime. Et [R:vérifier-que-l-abstraction-a-été-compilée] couvre le cas où l'optimiseur a supprimé le travail que le benchmark chronométrait -- une mesure à chaud de rien du tout.

Une file sans limite convertit la surcharge en latence au lieu d'un refus [R:une-file-sans-limite-n-a-pas-de-comportement-en-surcharge]

Écrit pour les publics Rust et Java.

Écrit à partir de la loi de Little (J. D. C. Little, 1961) : L = lambda W, donc pour une file qui ne refuse jamais, l'attente croît sans limite une fois que le taux d'arrivée dépasse le taux de service.

Décidez ce que fait la file lorsque la consommation est en retard sur la production. Croître n'est pas une décision.

La profondeur de la file est la latence -- c'est la loi de Little et non une heuristique : avec des arrivées au taux lambda et une longueur moyenne de file L, l'attente moyenne est L/lambda, donc un L sans limite est une attente sans limite. Une file sans limite de capacité n'a donc pas de limite de latence, et une exigence de latence exprimée en amont de celle-ci n'est maintenue par rien.

La défaillance a une forme caractéristique qui la rend tardive à diagnostiquer. Les producteurs ne bloquent jamais, donc rien en amont ne signale de problème ; le débit semble correct, car chaque élément est finalement traité ; et le seul symptôme est que la latence augmente de façon monotone. Au moment où elle est visible, le retard accumulé fait un travail dont personne ne veut -- chaque élément consommé est déjà obsolète, donc la capacité est dépensée à fournir des réponses à des questions dont le moment est passé, c'est pourquoi le système ne récupère pas lorsque la charge revient à la normale. Il doit d'abord se vider, au même rythme qui a pris du retard.

Donc, limitez la file et choisissez le comportement à la limite. Quatre sont légitimes et le choix est une décision de domaine, pas une décision technique :

  • Bloquer le producteur. Contre-pression réelle -- le consommateur le plus lent fixe le rythme pour tout le monde, et la pression se propage jusqu'à celui qui peut réellement la dissiper. Erroné là où le producteur ne doit pas être arrêté, ce qui est le régime auquel [R:pas-de-blocage-dans-une-fenêtre-de-publication] se rapporte.
  • Supprimer le plus ancien. Correct pour les données de marché, les lectures de capteurs, la télémétrie : un tick obsolète a une valeur négative et le plus récent est le seul qui vaille la peine d'être conservé.
  • Supprimer le plus récent. Correct là où le retard est une file d'attente équitable et où les nouveaux arrivants n'ont reçu aucune promesse.
  • Rejeter et le dire. Correct pour la saisie de commandes et tout ce qu'un appelant doit connaître du sort de son appel. Une commande supprimée silencieusement est pire qu'une refusée de toute la largeur de la confiance que l'appelant a placée dans l'appel.

Ne pas choisir sélectionne un cinquième comportement que personne ne choisirait délibérément : tout absorber, dégrader la latence de chaque élément de manière égale, et échouer finalement en épuisant la mémoire -- ou, sur un runtime managé, par une pause de collecte causée par le retard lui-même, ce qui ralentit davantage le consommateur et est la chose la plus proche dans la production d'une boucle de rétroaction avec le mauvais signe.

La structure sans limite reste le bon choix là où la limite réside véritablement ailleurs -- une file dont les producteurs sont limités en débit en amont, ou dont le consommateur est prouvé plus rapide que tout taux d'arrivée possible. Dites lequel, dans le code, à côté de la file. Une file sans limite avec une raison énoncée est une décision ; une sans est l'absence d'une décision portant la même forme.

Vérification du mode de défaillance : si la consommation est en retard sur la production pendant dix secondes, que fait cette file et qui le découvre ? Si la réponse est « elle croît » et « personne », il n'y a pas de comportement de surcharge ici, seulement un symptôme de surcharge en attente.

Une structure n'est correcte que sous le régime de concurrence que sa preuve a supposé [R:une-structure-conserve-le-régime-sous-lequel-elle-a-été-prouvée]

Écrit pour les publics Rust et Java.

Écrit à partir de Dmitry Vyukov, file MPSC intrusive basée sur des nœuds, 1024cores -- où le régime est énoncé dans le nom de l'algorithme et nulle part dans son code.

Lisez le régime à partir de l'argument de correction de la structure, puis nommez ce qui, dans le code, empêche de le violer. « La documentation dit monoconducteur » n'est pas un mécanisme.

MPSC signifie multi-producteur, mono-conducteur, et le mono est une précondition plutôt qu'une description. mpscq_pop de Vyukov lit self->tail, marche à partir de lui, et le réécrit -- un chargement simple et un stockage simple, non synchronisés, car on a supposé qu'exactement un thread les exécutait. Deux consommateurs s'y entrelacent et tous deux peuvent retourner le même nœud, ou l'un peut perdre un nœud entièrement. Aucun des deux résultats ne déclenche quoi que ce soit. Il n'y a pas d'assertion, pas de verrou à disputer, pas de chemin d'erreur : la file retourne des valeurs plausibles et le programme continue avec un élément dupliqué ou disparu.

Ce silence est tout le problème, et il est aggravé par la charge. Deux consommateurs exécutant cent opérations peuvent ne jamais s'entrelacer dans la fenêtre qui compte ; la suite de tests passe, le test de trempage en préproduction passe, et l'entrelacement arrive avec le trafic de production. Un défaut qui n'apparaît que dans des conditions que vous ne pouvez pas reproduire est impossible à distinguer, de l'extérieur, d'un défaut ailleurs.

Donc, mettez le régime dans le type, là où le langage le permet. Distribuez exactement un handle de consommateur à la construction ; ne le rendez ni Clone ni Sync ; prenez &mut self sur pop pour que le vérificateur d'emprunt enforce l'exclusivité. Alors « deux consommateurs » n'est pas un bogue à trouver mais un programme qui ne compile pas -- [R:rendre-les-états-invalides-non-représentables], et [R:typestate-pour-les-protocoles] là où le régime change au cours de la durée de vie d'une structure. C'est l'instance au plus fort effet de levier de cette discipline dans tout le domaine, car le contrôle alternatif est une phrase dans un commentaire d'en-tête et la défaillance qu'il empêche est non attribuable.

Là où le langage ne le portera pas -- Java n'a pas un tel handle -- le contrôle doit être quelque chose qui s'exécute réellement : enregistrez le thread propriétaire lors de la première utilisation et affirmez-le sur chaque appel suivant dans une build de débogage, ou acheminez toute la consommation via un seul objet dont la construction est le seul endroit où l'invariant est énoncé. Un commentaire n'est pas un contrôle ([R:garantie-a-besoin-d-un-lecteur]).

La classe est plus large que les files, et les autres membres échouent de la même manière. Un anneau SPSC avec deux producteurs. Une structure documentée comme non réentrante, entrée depuis un gestionnaire de signal ou un callback. Un HashMap atteint depuis deux threads parce que le champ qui le contenait est devenu partagé trois refactorings plus tard. Un itérateur maintenu sur une mutation. Dans chaque cas, le régime était une hypothèse de l'argument de correction et rien dans l'artefact ne l'enregistre.

Vérification du mode de défaillance : quel régime l'argument de correction de cette structure suppose-t-il, et qu'est-ce qui échouerait si je le violais maintenant ? Si la réponse honnête est « le nom le dit » ou « nous faisons attention », la structure est non protégée et la prochaine personne à l'utiliser ne le saura pas.

La mémoire qui n'a jamais été touchée n'est pas encore de la mémoire [R:alloué-n-est-pas-résident]

Écrit pour les publics Rust et Java.

Écrit à partir d'Ulrich Drepper, « Ce que tout programmeur devrait savoir sur la mémoire » (2007), sur la pagination à la demande et le coût d'un premier toucher.

Pré-allouer n'est pas préparer. Une allocation réussie achète de l'espace d'adressage ; la page arrive au premier toucher, et le toucher est ce qui coûte.

malloc et mmap rendent des adresses virtuelles. Les pages physiques sont fournies paresseusement : la première écriture sur chaque page piège dans le noyau, qui trouve un cadre, le met à zéro -- car les données de l'ancien occupant ne doivent pas fuir -- et le mappe. Donc, un tampon d'un gigaoctet alloué au démarrage et écrit pour la première fois sur le chemin critique contient environ un quart de million de défauts de page, arrivant un par page, répartis sur le premier passage sur celui-ci. Toute assertion sans allocation sur ce chemin est satisfaite et chacune d'elles mesure la mauvaise chose : rien n'a appelé l'allocateur.

La résidence n'est pas non plus permanente une fois atteinte. Les pages peuvent être réclamées sous pression mémoire ou échangées, donc un processus de longue durée qui a touché ses tampons au démarrage puis les a laissés inactifs peut à nouveau faillir sur un chemin qui a été sans défaut pendant une semaine. C'est pourquoi le remède a deux moitiés et pourquoi ne faire que la première est une erreur courante et coûteuse.

Donc, pour tout ce qui se trouve sur un chemin avec une échéance : écrivez sur chaque page que le chemin touchera, au démarrage, avant que tout travail réel n'arrive -- le toucher doit être une écriture, car une lecture d'une page non touchée peut être servie par la page zéro partagée et ne prouve rien. Ensuite, verrouillez-la : mlockall(MCL_CURRENT | MCL_FUTURE), et pas de swap sur la machine. Sur la JVM, -XX:+AlwaysPreTouch fait la première moitié pour le tas et vaut le démarrage plus lent précisément parce que le coût est déplacé hors du chemin qui a une limite.

Les grandes pages changent l'arithmétique dans les deux sens et méritent leur propre décision plutôt qu'une valeur par défaut. Moins de pages, plus grandes signifie moins de défauts et moins de pression TLB ; chaque défaut est plus grand, et les grandes pages transparentes apportent un chemin de défragmentation qui peut bloquer un thread pendant des millisecondes exactement au mauvais moment. Les grandes pages explicites réservées au démarrage évitent cela ; laisser THP sur always et espérer est la forme que ce domaine continue de devoir nommer.

Touchez depuis le thread qui possédera la mémoire, pas depuis le thread qui fait la configuration. Sur une machine multi-socket, la première écriture décide également du placement : une page est allouée sur le nœud de celui qui la provoque, donc la façon évidente de satisfaire cette règle -- une boucle dans le thread d'initialisation parcourant chaque tampon -- met chaque page sur le nœud de ce thread, et un travailleur épinglé sur l'autre socket lit ensuite tout cela sur l'interconnexion à environ deux fois la latence locale. Le chemin est maintenant sans défaut et entièrement distant : un état stable plus lent acheté avec un premier message plus rapide. Épinglez d'abord les threads propriétaires, puis laissez chacun d'eux toucher ce qu'il utilisera.

Vérification du mode de défaillance : chaque page que ce chemin touchera-t-elle été écrite, par le thread qui la lira, et qu'est-ce qui l'empêche d'être réclamée ? Trois questions, car ne répondre qu'à la première laisse un système qui est rapide jusqu'à ce qu'il devienne inactif, et ne répondre qu'aux deux premières en laisse un qui est rapide sur un seul socket.

[R:pas-d-allocation-sur-le-chemin-critique] est le frère et aucun ne subsume l'autre -- celui-ci concerne l'entrée dans l'allocateur, celui-ci concerne la mémoire qu'il a déjà retournée. [R:répondre-à-l-exigence-à-sa-couche] est pourquoi les remèdes sont tous une politique mémoire, un verrou ou un paramètre de démarrage : aucun choix de conteneur ou de crate n'atteint cela. [R:pas-de-blocage-dans-une-fenêtre-de-publication] est là où il mord le plus fort, car un défaut de page pris entre deux stockages d'un écrivain maintient la fenêtre ouverte pendant toute sa durée.

Répondre à une exigence à la couche où elle se trouve [R:répondre-à-l-exigence-à-sa-couche]

Écrit pour les publics Rust et Java.

Dites à quelle couche vit une exigence avant de choisir quoi que ce soit, et satisfaites-la là.

Une exigence concernant la machine -- ne pas perturber un cœur isolé voisin, ne pas défaillir une page sur le chemin d'envoi, ne pas prendre un verrou qu'un thread temps réel conteste -- n'est pas satisfaite en choisissant un crate plus rapide, un transport mieux réputé ou une API asynchrone. Ce sont des décisions au niveau de la bibliothèque. Elles peuvent être correctes et peuvent même aider, mais elles ne maintiennent pas une contrainte au niveau système, et les traiter comme si elles le faisaient convertit une question ouverte en une question fermée avec rien derrière.

L'échec n'est pas le mauvais choix ; c'est la mauvaise écriture comptable. Une contrainte marquée comme satisfaite cesse d'être examinée. Une contrainte ouverte est encore...

Individual skills in this repo

This repo contains 3 individual skills — each has its own dedicated page.

george-andrikopoulos/domain-java

Engineering discipline for working in java. Covers: a speculated path deoptimises when the input changes; a view is not a copy; a wrapper type is not free here; close what you open; design for inheritance or forbid it; equality is one contract; exceptions name what failed; identity is not equality for boxes; no reference to internals escapes; null is not a value; publish safely or not at all; seal the alternatives; serializable is a second constructor

george-andrikopoulos/domain-rust

Engineering discipline for working in rust. Covers: design types first; borrow in signatures; module visibility is deliberate; must use on consequential returns; newtype liberally; parse wide then range check; seal closed trait sets; typestate builder for required fields; typestate for protocols; verify the abstraction compiled away; async all the way; errors name what failed; justify every clone; no let underscore on a local; parse dont validate; private fields only; xplat fixtures; no...

george-andrikopoulos/global

Engineering discipline that applies to every project and language. Covers: guarantee needs a reader; verdict survives the channel; check the claim you inherit; doc currency; measure the claim not a subset; no silent spend; prefer by construction; repair the lying artefact; signal needs a consequence; verify through production path; detector excludes own definitions; an ambiguous write target is a question; anchor outside the checked partys reach; delegate a fan out; diff against the known...

Skills associés