Selenium Grid : pourquoi deux nœuds divergent
Une session atterrit sur le nœud qui avait un slot libre. Consignez ce que le profil fixe et ce que l’hôte décide encore, pour attribuer un écart à un nœud.
BotBrowser Team
Vous voulez la documentation structurée pour Documentation ?
Cet article fait partie de la bibliothèque éditoriale. Pour les étapes de configuration, la référence et les mises à jour continues, passez directement à la section docs.
Un test Selenium Grid ne s’exécute pas sur la machine qui l’a soumis. Le Distributor affecte la session à un slot sur un nœud, et le navigateur qui exécute le test est celui installé sur ce nœud. Deux nœuds qui lancent le même test, le même code et la même version de navigateur peuvent tout de même produire des résultats différents, car un slot est une unité d’ordonnancement et non la garantie d’un environnement identique. La première question après un tel échec n’est pas quelle assertion a cassé, mais quel nœud l’a exécuté, et la réponse doit venir d’un relevé plutôt que d’une nouvelle exécution. BotBrowser fixe les valeurs d’identité qu’une page lit à travers un profil, et ne change ni les pixels qu’un hôte rastérise ni l’horloge qu’un hôte tient : ce sont donc exactement les champs qui diffèrent encore entre les nœuds.
Résumé
- Un slot Grid est une unité d’ordonnancement. Le Distributor affecte une session au slot qui correspond à la demande, donc le nœud qui exécute un test est choisi à l’exécution, pas par le fichier de test.
- Séparez le relevé en champs de contrôle et champs d’hôte. En mode WebGL par défaut, un champ de contrôle comme la chaîne de renderer UNMASKED ou
navigator.hardwareConcurrencydevrait être identique lorsque le même profil est appliqué ; s’il diffère, ce sont les profils ou les options de lancement qui diffèrent. - Un champ d’hôte comme un hash de pixels ou une durée écoulée peut encore différer avec un profil parfait, car le profil ne rastérise pas l’image et ne tient pas l’horloge.
- Consignez le session id avec chaque tentative. La table des capabilities ne contient pas de node id, donc l’identité du nœud vient d’une requête GraphQL ou de l’interface du Grid pour cette session.
- Un échec qui se concentre sur un nœud accrédite un artefact d’hôte ; un échec réparti uniformément entre les nœuds désigne le code, les données ou l’application.
Sommaire
- Ce qu’est un nœud et ce qu’est un slot
- Deux types de champs dans un même relevé
- Capturer un relevé par tentative
- Attribuer un échec à un nœud
- Distinguer contention et divergence
- Ce qu’un profil fixe et ce qu’il ne peut pas fixer
- Conclusion pratique
Ce qu’est un nœud et ce qu’est un slot
Le Grid se compose d’un Router, d’une New Session Queue, d’un Distributor, d’une Session Map et d’un ou plusieurs Nodes. Le Router transmet une requête entrante soit à la file, soit au nœud qui possède déjà la session, et le Distributor affecte une nouvelle session à un slot libre. Chaque nœud a un ou plusieurs slots, et chaque slot porte un stereotype : l’ensemble minimal de capabilities qu’une requête doit satisfaire pour que le Distributor l’envoie à ce nœud. La Session Map conserve la correspondance entre un session id et l’adresse du nœud qui l’exécute.
Cette structure explique la première leçon d’un test Grid instable. Une requête avec les mêmes capabilities peut être traitée par n’importe quel nœud dont le stereotype de slot correspond, donc des exécutions consécutives d’un même test peuvent se dérouler sur des hôtes différents sans aucune modification du test. À l’enregistrement, un nœud déclare automatiquement les drivers de navigateur présents dans son propre PATH et crée un slot par CPU disponible pour les navigateurs fondés sur Chromium et Firefox, tandis que Safari reçoit un seul slot. Deux nœuds avec des nombres de cœurs différents annoncent donc un nombre de slots différent du point de vue du même test.
L’enregistrement d’un nœud porte aussi un UUID nodeId et un objet osInfo avec arch, name et version, que l’interface du Grid et les requêtes GraphQL exposent. Ces deux champs transforment « le test a échoué » en « le test a échoué sur ce nœud ». Les lire coûte une requête et tranche un débat qui dure sinon des jours.
La file compte pour la même raison. Quand tous les slots correspondants sont occupés, la nouvelle session attend au lieu d’échouer, donc un test lancé en période de pointe démarre plus tard que le même test exécuté seul. Un délai mesuré depuis le démarrage du processus de test inclut donc le temps de file, et une suite qui fixe ses propres échéances sans le savoir signalera un retard d’ordonnancement comme un timeout applicatif. Le Distributor tient aussi un modèle du Grid, et la documentation d’architecture note que ce modèle peut se désynchroniser de la réalité, par exemple juste après le démarrage du Distributor ; une session routée vers un slot qui ne répond plus ressemble à une panne réseau vue de l’intérieur du test.
Aucun de ces composants ne compare deux nœuds. Le Grid distribue le travail et suit les sessions ; il ne dit pas si le nœud sept et le nœud douze produisent les mêmes pixels. Cette comparaison doit être intégrée au relevé du test lui-même.
Deux types de champs dans un même relevé
La plupart des comparaisons de nœuds échouent avant de commencer parce qu’elles mélangent deux questions dans un seul tableau. L’une demande si l’environnement déclaré est bien arrivé ; l’autre si l’hôte se comporte de la même façon. Un profil répond à la première et ne peut pas répondre à la seconde, donc le relevé doit les séparer.
| Champ | Qui décide de la valeur | Ce que signifie un écart |
|---|---|---|
| Chaînes vendor et renderer UNMASKED | Le profil, ou l’hôte en mode --bot-webgl=real | Des profils différents, ou deux hôtes avec des piles WebGL différentes |
navigator.hardwareConcurrency | Le profil par défaut, ou l’hôte avec --bot-cores=real | Les profils diffèrent, ou un nœud a été lancé avec un override de cœurs réels |
| Dimensions d’écran, locale, fuseau | Le profil, sauf override par session | Une différence de configuration, pas une différence de rendu |
| Version du navigateur et du driver | Ce que le nœud a enregistré | Une correspondance de capabilities qui ignore la version a routé la requête vers deux builds |
Hash de pixels d’un dessin fixe, --bot-webgl=real | Le rastériseur de l’hôte | Une vraie différence d’hôte qui survit à un profil correct et à un mode WebGL réel |
| Durée d’une étape fixe | Le CPU, la charge et le réseau de l’hôte | De la contention ou une différence matérielle, pas une différence d’identité |
Les trois premières lignes sont des champs de contrôle lorsque le lancement utilise le mode WebGL par défaut, et ils devraient être identiques sur les deux nœuds lorsque le même profil et les mêmes flags de lancement sont utilisés, donc un écart à cet endroit est un bug de configuration dont le correctif est simple. La ligne du renderer porte une réserve qui compte pour cette mesure : en mode --bot-webgl=real, WebGL est servi par l’hôte sans protection, donc les chaînes UNMASKED suivent la pile de l’hôte et la ligne cesse d’être un champ de contrôle. C’est pourquoi le résumé de pixels a besoin du mode real et pourquoi la chaîne de renderer n’a pas sa place dans la même comparaison en tant que contrôle. La documentation de configuration des profils BotBrowser liste les valeurs d’identité qu’un profil fournit, notamment les dimensions d’écran et de fenêtre, les polices, WebGL et WebGPU, les périphériques média et les voix de synthèse, et sa documentation de gestion des profils liste des signaux matériels comme la mémoire de l’appareil, le nombre de cœurs CPU et le modèle de GPU parmi les valeurs qu’un profil porte. Le flag --bot-cores est documenté comme choisissant entre le nombre revendiqué par le profil et le nombre réel de l’hôte, et la valeur par défaut conserve la revendication du profil.
Les deux dernières lignes sont des champs d’hôte, et c’est là que se trouve la surprise. Un profil contrôle ce que la page apprend du GPU, pas ce que le driver du GPU calcule réellement. La documentation du backend GPU Linux de BotBrowser l’affirme directement : différents rastériseurs logiciels produisent une sortie de pixels différente pour le même shader, donc les octets exacts issus de readPixels ou de toDataURL diffèrent entre SwiftShader et Mesa llvmpipe, tandis qu’en mode WebGL par défaut les chaînes vendor et renderer UNMASKED restent contrôlées par le profil et ne changent pas lors de cette migration. Avec --bot-webgl=real, cette garantie est délibérément désactivée, donc la même migration déplace à la fois les octets de pixels et les chaînes rapportées. Une comparaison de canvas entre deux nœuds est donc une comparaison d’hôtes même lorsque les valeurs d’identité correspondent parfaitement.
Garder les deux groupes dans un même relevé rend le diagnostic possible. Un écart dans le groupe de contrôle dit que les deux exécutions n’étaient pas la même expérience ; un écart dans le groupe d’hôte dit que c’était la même expérience sur deux machines différentes. Confondre les deux conduit les équipes à refaire tourner un profil alors que le problème réel est un rastériseur, ou à réécrire un test alors que le problème réel est un champ de profil manquant.
Les capabilities qu’un test envoie décrivent une intention ; les valeurs qu’une page lit décrivent un résultat. Les deux ne sont pas identiques, et l’écart entre elles est l’endroit où se cachent la plupart des divergences entre nœuds. Une requête peut demander Chrome sans version, demander un nom de plateforme sans version, et demander une taille de fenêtre que le nœud ajuste discrètement pour tenir sur son écran. Ce que la page rapporte ensuite est ce que l’application reçoit réellement, et c’est donc ce qui a sa place dans le relevé.
L’exemple qui suit collecte une courte liste fixe depuis la session en cours : trois champs de contrôle, un résumé de pixels et une durée. Il imprime une ligne, ce qui suffit à comparer deux nœuds et reste assez court pour être joint à un rapport sans exposer de données de compte.
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
options = Options()
options.add_argument("--bot-gpu-emulation=false")
options.add_argument("--bot-webgl=real")
driver = webdriver.Remote(command_executor="http://grid.internal:4444", options=options)
try:
driver.get("https://app.example.test/health")
values = driver.execute_script(
"""
const canvas = document.createElement('canvas');
canvas.width = 220; canvas.height = 90;
const gl = canvas.getContext('webgl');
const dbg = gl && gl.getExtension('WEBGL_debug_renderer_info');
const vs = [
'attribute vec2 p;',
'varying vec2 uv;',
'void main() { uv = p * 0.5 + 0.5; gl_Position = vec4(p, 0.0, 1.0); }',
].join('\n');
const fs = [
'precision mediump float;',
'varying vec2 uv;',
'void main() {',
' float a = (sin(uv.x * 37.0 + 1.7) + sin(uv.y * 23.0 + 3.1) + sin((uv.x + uv.y) * 61.0)) / 3.0;',
' float b = fract(uv.x * 41.0) * fract(uv.y * 29.0);',
' gl_FragColor = vec4(0.5 + 0.45 * a, b, 0.5 - 0.45 * a, 1.0);',
'}',
].join('\n');
// Measured with --bot-gpu-emulation=false --bot-webgl=real. The defaults would answer
// this draw from the profile GPU layer, and the digest would describe the profile.
let digest = null;
if (gl) {
const compile = (type, src) => {
const s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
return gl.getShaderParameter(s, gl.COMPILE_STATUS) ? s : null;
};
const program = gl.createProgram();
const vertex = compile(gl.VERTEX_SHADER, vs);
const fragment = compile(gl.FRAGMENT_SHADER, fs);
if (!vertex || !fragment) return { pixels: null, pixels_error: 'compile_failed' };
gl.attachShader(program, vertex);
gl.attachShader(program, fragment);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) return { pixels: null, pixels_error: 'link_failed' };
gl.useProgram(program);
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
const p = gl.getAttribLocation(program, 'p');
gl.enableVertexAttribArray(p);
gl.vertexAttribPointer(p, 2, gl.FLOAT, false, 0, 0);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
const bytes = new Uint8Array(220 * 90 * 4);
gl.readPixels(0, 0, 220, 90, gl.RGBA, gl.UNSIGNED_BYTE, bytes);
let h = 2166136261;
for (let i = 0; i < bytes.length; i += 4) h = Math.imul(h ^ bytes[i + 1], 16777619) >>> 0;
digest = h.toString(16).padStart(8, '0');
}
const started = performance.now();
for (let i = 0; i < 20000; i += 1) Math.sqrt(i);
return {
renderer: dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : null,
cores: navigator.hardwareConcurrency,
lang: navigator.language,
dpr: window.devicePixelRatio,
pixels: digest,
elapsed: Math.round((performance.now() - started) * 100) / 100,
};
"""
)
print({"session": driver.session_id, "browser": driver.capabilities.get("browserVersion"), **values})
finally:
driver.quit()
La liste est volontairement courte et volontairement mixte. cores, lang et dpr sont des champs de contrôle : un profil les fournit, donc ils devraient correspondre sur les deux nœuds et un écart est la première chose à examiner. renderer n’est un champ de contrôle qu’en mode WebGL par défaut et passe du côté de l’hôte avec --bot-webgl=real, ce qui explique que le relevé d’exemple qui suit le laisse hors du groupe de contrôle. pixels est un champ d’hôte : il dessine un seul triangle fixe plein écran dont le fragment shader mélange trois termes sinus avec un produit de fract, relit chaque pixel avec readPixels et replie le canal vert en un court résumé FNV-1a. L’interpolation et la couverture sous-pixel sont précisément là où deux rastériseurs divergent, donc le shader doit varier sur la surface : une couleur constante, et un clear en particulier, renvoie les mêmes octets sur chaque backend et ne mesure rien. Deux conditions de lancement donnent à la mesure le sens qu’elle revendique. L’émulation GPU doit être désactivée et WebGL doit être en mode real : les valeurs par défaut répondent à un dessin WebGL depuis la couche du profil, qui contrôle la sortie du shader et la lecture de l’image en plus des chaînes d’identité, donc une exécution laissée sur les valeurs par défaut mesure le profil puis le signale comme champ d’hôte. Le relevé porte aussi devicePixelRatio, car le résumé dépend du nombre de pixels de la surface ; un nœud qui rend la même scène à une autre échelle produit un résumé différent pour une raison sans rapport avec son rastériseur. elapsed est aussi un champ d’hôte, et un champ grossier, donc il sépare la contention de l’identité sans prétendre être un benchmark.
Deux détails d’implémentation changent le sens du relevé. La table des capabilities ne contient pas de node id, donc le script imprime le session id et l’identité du nœud est résolue ensuite depuis le Grid ; supposer qu’une capability nodeId existe produit None sur chaque nœud et supprime silencieusement le seul champ qui rend l’attribution possible. Et la chaîne de renderer n’est disponible que si l’extension debug-renderer existe ; un navigateur qui la retient renvoie null, ce qui est en soi le signal que la valeur a été masquée plutôt que rapportée.
Capturer un relevé par tentative
Le relevé qui rend la divergence visible est petit : l’identité du nœud, l’identité du build et les deux groupes de champs. Le constructeur qui suit est une fixture construite montrant à quoi ressemble un test sur deux nœuds qui partagent un profil.
// Constructed fixture: one test, two attempts, one shared profile, two hosts.
const attempts = [
{
n: 1,
session: 'd41f-07',
browser: 'Chrome 156.0.0.0',
control: { cores: 8, lang: 'en-US', dpr: 1 },
host: { pixels: 'fc0472cd', elapsed: 12.4 },
},
{
n: 2,
session: 'd41f-12',
browser: 'Chrome 156.0.0.0',
control: { cores: 8, lang: 'en-US', dpr: 1 },
host: { pixels: '3b9e51a0', elapsed: 9.1 },
},
];
const controlKeys = Object.keys(attempts[0].control);
const controlDrift = controlKeys.filter(k => attempts[0].control[k] !== attempts[1].control[k]);
const pixelsMissing = attempts.some(a => a.host.pixels == null);
const pixelDrift = !pixelsMissing && attempts[0].host.pixels !== attempts[1].host.pixels;
const elapsedDrift = attempts[0].host.elapsed !== attempts[1].host.elapsed;
const verdict = controlDrift.length
? 'configuration_drift'
: pixelsMissing
? 'pixel_unavailable'
: pixelDrift
? 'pixel_divergence'
: elapsedDrift
? 'contention'
: 'no_divergence';
const lines = [
`attempts=${attempts.length} browser_same=${attempts[0].browser === attempts[1].browser}`,
...controlKeys.map(k => `control=${k} same=${attempts[0].control[k] === attempts[1].control[k]}`),
`host=pixels same=${pixelDrift ? 'false' : 'true'}`,
`host=elapsed same=${elapsedDrift ? 'false' : 'true'}`,
`verdict=${verdict}`,
];
// The non-default branches are what separate the causes; print one line each so a
// reader can check the rule rather than trust it.
const cases = [
{ pixels: 'p', elapsed: 1 },
{ pixels: 'q', elapsed: 1 },
{ pixels: 'p', elapsed: 9 },
{ pixels: 'p', elapsed: 1 },
{ pixels: null, elapsed: 1 },
];
for (let i = 0; i + 1 < cases.length; i += 1) {
const a = cases[i];
const b = cases[i + 1];
const missing = a.pixels == null || b.pixels == null;
const branch = missing
? 'pixel_unavailable'
: a.pixels !== b.pixels
? 'pixel_divergence'
: a.elapsed !== b.elapsed
? 'contention'
: 'no_divergence';
const samePixels = a.pixels == null || b.pixels == null ? 'null' : a.pixels === b.pixels;
lines.push(`branch=${branch} pixels_same=${samePixels} elapsed_same=${a.elapsed === b.elapsed}`);
}
console.log(lines.join('\n'));
La sortie construite est le point, en deux parties. Chaque champ de contrôle correspond, car les deux nœuds ont chargé le même profil, tandis que le résumé de pixels diffère : le verdict est donc pixel_divergence et non un défaut de configuration. Les branches imprimées montrent ensuite la règle qui garde le verdict honnête : un relevé dont les pixels correspondent et dont la durée diffère est signalé comme contention, et non comme une différence de rendu entre nœuds.
attempts=2 browser_same=true
control=cores same=true
control=lang same=true
control=dpr same=true
host=pixels same=false
host=elapsed same=false
verdict=pixel_divergence
branch=pixel_divergence pixels_same=false elapsed_same=true
branch=pixel_divergence pixels_same=false elapsed_same=false
branch=contention pixels_same=true elapsed_same=false
branch=pixel_unavailable pixels_same=null elapsed_same=true
Trois règles gardent le relevé utile. Lisez les valeurs dans la session plutôt que dans les capabilities demandées par le test, car la requête décrit une intention et la page décrit ce qui est arrivé. Classez chaque champ comme contrôle ou hôte avant toute comparaison, car les deux types d’écart ont des propriétaires différents. Et gardez une liste de champs courte et fixe, pour que deux relevés se comparent mécaniquement plutôt qu’à l’œil.
Attribuer un échec à un nœud
L’attribution est une question de distribution. Collectez le nodeId pour chaque tentative du test en échec et comptez la répartition des échecs. Un échec qui apparaît sur un nœud et jamais sur les autres accrédite un artefact d’hôte, et l’étape suivante consiste à comparer les champs d’hôte de ce nœud à ceux d’un nœud qui passe. Un échec qui apparaît sur chaque nœud à peu près au même taux n’est pas un problème de nœud, et le temps passé à comparer les hôtes est du temps retiré au code, à la fixture ou à l’application.
Une seconde distribution compte autant. Quand un nœud annonce plus de slots que l’hôte ne peut en servir confortablement, plusieurs sessions partagent une machine et chacune subit plus de contention. Un test qui échoue seulement quand le nœud est à capacité est un artefact d’ordonnancement plutôt qu’une différence de rendu, et le relevé les sépare parce que la contention déplace le champ elapsed tandis qu’une différence de rastériseur déplace le champ pixels.
Le guide d’intégration des profils Selenium couvre qui possède le lancement, le répertoire user-data et la fermeture de session sur un nœud ; l’analyse des attentes couvre la moitié temporelle du même diagnostic. L’étape qui précède les deux consiste à décider si la différence vient réellement de l’hôte.
Distinguer contention et divergence
Un nœud qui exécute quatre sessions à la fois ne se comporte pas comme un nœud qui en exécute une. Le Distributor considère un nœud plein lorsqu’il atteint maxSessionCount, et par défaut un nœud annonce un slot par CPU disponible pour les navigateurs fondés sur Chromium et Firefox, donc un hôte à seize cœurs peut accepter seize sessions simultanées sauf configuration contraire. Chacune de ces sessions se dispute la même bande passante mémoire, le même rastériseur et le même disque, et un test qui mesure quoi que ce soit du temps verra la contention avant toute différence d’identité.
La distinction compte parce que les deux formes d’échec se ressemblent dans un rapport et appellent des correctifs différents. Un échec de contention n’apparaît que lorsque le nœud est occupé, se déplace entre les tests plutôt qu’entre les nœuds, et disparaît en exécution sérielle. Un échec de divergence apparaît chaque fois qu’un nœud particulier exécute le test, que le nœud soit occupé ou inactif, et se reproduit en exécution sérielle sur ce nœud. Consigner le nombre de sessions concurrentes à côté de l’identité du nœud est ce qui les sépare, car ce nombre est la variable qui a changé.
La contention est aussi la raison pour laquelle un nombre de nœuds est un mauvais proxy de capacité. Ajouter des nœuds à un Grid augmente le nombre d’hôtes disponibles, mais ne change pas le nombre de sessions que chaque hôte accepte, et un parc à court de mémoire par session continuera de produire les mêmes échecs de contention à plus grande échelle. Le réglage maxSessionCount et la mémoire disponible par session sont les deux nombres qui décident cela, et les deux appartiennent au relevé du nœud plutôt qu’à la mémoire d’une équipe.
Il existe une troisième forme qui n’est ni contention ni divergence : un test qui dépend de données qui changent avec le temps. Un prix, une date ou une longueur de liste que la fixture supposait fixe échouera selon un calendrier sans rapport avec le nœud qui l’a exécuté, et le relevé le montre parce que les tentatives en échec se répartissent uniformément entre les nœuds et entre les niveaux de charge. Le modèle d’attribution à quatre causes traite ce cas comme une cause à part, précisément pour cette raison.
Ce qu’un profil fixe et ce qu’il ne peut pas fixer
La moitié identité du relevé est la raison d’être d’un profil. BotBrowser documente les valeurs d’identité qu’un profil fournit et les applique avant tout script de page, et il ne contrôle pas directement le rastériseur ni l’horloge fournis par l’hôte, donc en mode WebGL par défaut les chaînes vendor et renderer UNMASKED, le nombre de cœurs rapporté, les dimensions d’écran, la liste de polices et la pile de langue sont identiques sur chaque hôte qui charge le même profil avec les mêmes flags de lancement. Avec --bot-webgl=real, les chaînes suivent l’hôte à la place, la seule ligne qui sort du groupe de contrôle dans ce mode. C’est ce qui rend une comparaison de champs de contrôle significative, et c’est pourquoi un écart de contrôle pointe vers la configuration plutôt que vers le matériel.
L’autre moitié est hors de portée par conception. Un profil ne rastérise pas l’image, donc les octets qu’un backend logiciel écrit pour un shader diffèrent entre backends même avec un profil identique. Il n’ordonnance pas non plus le travail, donc une étape fixe prend plus de temps sur un hôte plus chargé ou plus lent. Le guide multiplateforme de BotBrowser indique que la portabilité exige la même version de BotBrowser, le même fichier de profil et les mêmes flags de lancement, et son guide de serveur headless documente qu’un serveur sans GPU rend via l’un de plusieurs backends logiciels dont les compromis diffèrent. Une suite qui compare la sortie de pixels entre nœuds doit donc encore consigner le backend et le nœud, et une suite qui mesure la latence doit encore consigner la charge.
FAQ
Pourquoi le même test passe-t-il sur un nœud et échoue-t-il sur un autre ?
Le Distributor a affecté les deux exécutions à des slots différents. Comparez les deux relevés de nœuds avant de toucher au test : d’abord les champs de contrôle, car un écart signifie que les profils ou les flags de lancement différaient, puis les champs d’hôte, car un écart signifie que deux machines ont rendu ou chronométré le même travail différemment.
Un profil peut-il rendre deux nœuds Grid identiques ?
Non. Un profil fixe les valeurs d’identité qu’une page lit, ce qui élimine une grande classe de divergences, mais l’hôte fournit toujours le rastériseur, l’ordonnancement CPU et l’horloge. Des pixels identiques exigent le même backend de rendu en plus du même profil, et des temps identiques exigent la même charge.
Pourquoi la chaîne de renderer peut-elle correspondre alors que les pixels diffèrent ?
Parce que des couches différentes et des modes de lancement différents les décident. En mode WebGL par défaut, la chaîne de renderer est une valeur d’identité fournie par le profil, tandis que les pixels sont ce que le rastériseur de l’hôte calcule, donc une chaîne identique et un résumé de pixels différent forment la paire attendue. La documentation du backend GPU de BotBrowser indique que dans ce mode les chaînes UNMASKED restent contrôlées par le profil lors d’une migration de backend pendant que les octets de pixels changent. Avec --bot-webgl=real, les chaînes suivent l’hôte à la place, donc sur deux nœuds dont les piles WebGL diffèrent, la chaîne et le résumé changent ensemble.
Combien de tentatives faut-il avant de qualifier un échec de spécifique à un nœud ?
Assez pour que l’échec soit absent des autres nœuds sous la même charge. Un seul échec sur un nœud et une seule réussite sur un autre restent une hypothèse ; un motif sur des tentatives répétées est une attribution.
Sources
- Selenium Grid architecture
- Selenium Grid components
- Selenium WebDriver drivers
- BotBrowser profile configuration
- BotBrowser profile management
- BotBrowser cross-platform profiles
- BotBrowser headless server setup
- BotBrowser Linux GPU backend selection
- BotBrowser CPU core scaling
Conclusion pratique
Un nœud qui diverge d'un autre peut aussi exécuter une autre compilation du navigateur, ce qui est une cause distincte avec un correctif distinct ; Chrome for Testing : épingler une compilation, pas une empreinte couvre la façon d'épingler et ce qu'un épinglage laisse dehors.
Consignez le session id, la version du navigateur et du driver, trois champs de contrôle, un résumé de pixels et une durée à chaque tentative de chaque test Grid. Comparez d’abord les champs de contrôle : un écart à cet endroit est un bug de configuration, pas un mystère matériel. Comparez ensuite les champs d’hôte : un écart est la différence qu’un profil ne peut pas supprimer, donc consignez le backend, la charge et le nœud au lieu de refaire tourner le profil. Traitez un échec qui se concentre sur un nœud comme l’indice d’un artefact d’hôte et un échec qui se répartit comme un problème de code ou de données.
Articles Connexes
Tests instables : quatre causes et leurs preuves
ComparaisonVérification publicitaire : région et langue cohérentes
DémarrageScraping avec Selenium : capacités et relevés
ComparaisonQue change un navigateur antidétection ?
DémarrageSelenium sans fenêtre : ChromeOptions et écarts
EmpreinteUnicode : quatre identités de texte lues par une page
Exécutez la même vérification dans BotBrowser
Chargez un profil, gardez dans le navigateur les signaux que vous venez de lire, puis confirmez le résultat sur votre propre machine avant le déploiement.