Volver al Centro de conocimiento
Comparación

Selenium Grid: por qué dos nodos discrepan

Una sesión llega al nodo que tenga un slot libre. Registra qué fija el perfil y qué decide aún el host, para atribuir la diferencia a un nodo, no a un commit.

BotBrowser Team

Documentación

Quieres la documentación estructurada de Documentación?

Este artículo forma parte de la biblioteca editorial. Para pasos de configuración, material de referencia y actualizaciones continuas, entra en la sección de docs.

Una prueba de Selenium Grid no se ejecuta en la máquina que la envió. El Distributor asigna la sesión a un slot de algún nodo, y el navegador que ejecuta la prueba es el instalado en ese nodo. Dos nodos que ejecutan la misma prueba, el mismo código y la misma versión de navegador pueden producir resultados distintos, porque un slot es una unidad de planificación y no una garantía de que el entorno sea idéntico. La primera pregunta tras un fallo así no es qué aserción se rompió, sino qué nodo la ejecutó, y la respuesta debe salir de un registro, no de una repetición. BotBrowser fija los valores de identidad que una página lee mediante un perfil, y no cambia los píxeles que un host rasteriza ni el reloj que un host mantiene, así que esos son exactamente los campos que aún difieren entre nodos.

Una prueba de Grid en dos nodos: el Distributor asigna la sesión a un slot libre, cada nodo registra campos de control que deberían coincidir y campos de host que pueden diferir, y la comparación produce un veredicto node_divergence.

TL;DR

  • Un slot de Grid es una unidad de planificación. El Distributor asigna una sesión al slot que coincida con la petición, así que el nodo que ejecuta una prueba se elige en tiempo de ejecución, no en el archivo de prueba.
  • Divide el registro en campos de control y campos de host. En el modo WebGL por defecto, un campo de control como la cadena de renderer UNMASKED o navigator.hardwareConcurrency debería ser idéntico cuando se aplica el mismo perfil; si difiere, difieren los perfiles o los flags de lanzamiento.
  • Un campo de host como un hash de píxeles o un tiempo transcurrido puede seguir difiriendo con un perfil perfecto, porque el perfil no rasteriza el fotograma ni mantiene el reloj.
  • Guarda el session id con cada intento. El mapa de capacidades no lleva un node id, así que la identidad del nodo sale de la consulta GraphQL o de la UI del propio Grid para esa sesión.
  • Un fallo que se agrupa en un nodo respalda un artefacto de host; un fallo que se reparte de forma uniforme entre nodos apunta al código, a los datos o a la aplicación.

Contenido

Qué son un nodo y un slot

El Grid se divide en un Router, una New Session Queue, un Distributor, un Session Map y uno o más Nodes. El Router reenvía una petición entrante a la cola o al nodo que ya posee la sesión, y el Distributor asigna una sesión nueva a un slot libre. Cada nodo tiene uno o más slots, y cada slot lleva un estereotipo: el conjunto mínimo de capacidades que una petición debe cumplir antes de que el Distributor la envíe a ese nodo. El Session Map mantiene la correspondencia entre un session id y la dirección del nodo que lo ejecuta.

Esa estructura explica lo primero que enseña una prueba de Grid intermitente. Una petición con las mismas capacidades puede ser atendida por cualquier nodo cuyo estereotipo de slot coincida, así que ejecuciones consecutivas de una prueba pueden correr en hosts distintos sin ningún cambio en la prueba. Cuando un nodo se registra, auto-registra los drivers de navegador disponibles en su propio PATH y crea un slot por cada CPU disponible para navegadores basados en Chromium y Firefox, mientras que Safari recibe un solo slot. Dos nodos con recuentos de núcleos distintos anuncian, por tanto, números distintos de slots desde el punto de vista de la misma prueba.

El registro de un nodo también lleva un UUID nodeId y un objeto osInfo con arch, name y version, que la UI de Grid y las consultas GraphQL exponen. Esos dos campos son los que convierten "la prueba falló" en "la prueba falló en este nodo". Leerlos cuesta una consulta y zanja una discusión que, de otro modo, dura días.

La cola importa por la misma razón. Cuando todos los slots coincidentes están ocupados, la sesión nueva espera en lugar de fallar, así que una prueba que corre en un periodo punta empieza más tarde que la misma prueba ejecutada sola. Un timeout medido desde el momento en que arrancó el proceso de prueba incluye, por tanto, el tiempo en cola, y una suite que fija sus propios plazos sin saberlo reportará un retraso de planificación como un timeout de la aplicación. El Distributor también mantiene un modelo del Grid, y la documentación de arquitectura señala que ese modelo puede quedar desincronizado con la realidad, por ejemplo justo después de que arranque el Distributor; una sesión enrutada a un slot que ya no responde parece un fallo de red desde dentro de la prueba.

Ninguno de estos componentes compara dos nodos. El Grid asigna trabajo y sigue sesiones; no dice si el nodo siete y el nodo doce producen los mismos píxeles. Esa comparación tiene que formar parte del registro de la propia prueba.

Dos tipos de campo en un registro

La mayoría de las comparaciones entre nodos fallan antes de empezar porque mezclan dos preguntas en una tabla. Una pregunta es si llegó el entorno declarado; la otra es si el host se comporta igual. Un perfil responde a la primera y no puede responder a la segunda, así que el registro tiene que separarlas.

CampoQuién decide el valorQué significa una diferencia
Cadenas de vendor y renderer UNMASKEDEl perfil, o el host en modo --bot-webgl=realPerfiles distintos, o dos hosts con pilas WebGL distintas
navigator.hardwareConcurrencyEl perfil por defecto, o el host con --bot-cores=realLos perfiles difieren, o un nodo se lanzó con una anulación de núcleos reales
Dimensiones de pantalla, locale, zona horariaEl perfil, salvo anulación por sesiónUna diferencia de configuración, no de renderizado
Versión de navegador y driverLo que registró el nodoUna coincidencia de capacidades que ignora la versión envió la petición a dos builds
Hash de píxeles de un dibujo fijo, --bot-webgl=realEl rasterizador del hostUna diferencia real de host que sobrevive a un perfil correcto y a un modo WebGL real
Tiempo transcurrido de un paso fijoCPU, carga y red del hostContención o una diferencia de hardware, no una diferencia de identidad

Las tres primeras filas son campos de control cuando el lanzamiento usa el modo WebGL por defecto, y deberían ser idénticas en ambos nodos cuando se usan el mismo perfil y los mismos flags de lanzamiento, así que una diferencia ahí es un fallo de configuración con una corrección barata. La fila del renderer lleva una advertencia que importa para esta medición: en modo --bot-webgl=real WebGL lo sirve el host sin protección, así que las cadenas UNMASKED siguen la pila del host y la fila deja de ser un campo de control. Por eso el resumen de píxeles necesita el modo real y la cadena de renderer no pertenece a la misma comparación como control. La documentación de configuración de perfiles de BotBrowser enumera los valores de identidad que aporta un perfil, incluidas las dimensiones de pantalla y ventana, las fuentes, WebGL y WebGPU, los dispositivos multimedia y las voces de síntesis, y su documentación de gestión de perfiles enumera señales de hardware como la memoria del dispositivo, el recuento de núcleos de CPU y el modelo de GPU entre los valores que lleva un perfil. El flag --bot-cores está documentado para elegir entre el recuento declarado por el perfil y el recuento real del host, y el valor por defecto mantiene la declaración del perfil.

Las dos últimas filas son campos de host, y aquí está la sorpresa. Un perfil controla lo que se le dice a la página sobre la GPU, no lo que el driver de GPU calcula realmente. La documentación del backend de GPU en Linux de BotBrowser lo afirma directamente: distintos rasterizadores de software producen salidas de píxeles distintas para el mismo shader, así que los bytes exactos que salen de readPixels o toDataURL difieren entre SwiftShader y Mesa llvmpipe, mientras que en el modo WebGL por defecto las cadenas de vendor y renderer UNMASKED siguen controladas por el perfil y no cambian con esa migración. Con --bot-webgl=real esa garantía se desactiva a propósito, así que la misma migración mueve tanto los bytes de píxeles como las cadenas reportadas. Una comparación de canvas entre dos nodos es, por tanto, una comparación de host incluso cuando los valores de identidad coinciden a la perfección.

Mantener ambos grupos en un mismo registro es lo que hace posible el diagnóstico. Una discrepancia en el grupo de control dice que las dos ejecuciones no fueron el mismo experimento; una discrepancia en el grupo de host dice que fueron el mismo experimento en dos máquinas distintas. Confundir ambos lleva a los equipos a cambiar un perfil cuando el problema real es un rasterizador, o a reescribir una prueba cuando el problema real es un campo de perfil ausente.

Las capacidades que envía una prueba describen una intención; los valores que lee una página describen un resultado. No son lo mismo, y la brecha entre ambos es donde se esconden la mayoría de las divergencias entre nodos. Una petición puede pedir Chrome sin versión, pedir un nombre de plataforma sin versión y pedir un tamaño de ventana que el nodo ajusta en silencio para encajar en su pantalla. Lo que la página reporta después es lo que la aplicación recibe realmente, así que eso es lo que pertenece al registro.

El código de captura recoge una lista corta y fija desde dentro de la sesión en ejecución: tres campos de control, un resumen de píxeles y un tiempo. Imprime una línea, suficiente para comparar dos nodos y lo bastante pequeña para adjuntarla a un informe sin filtrar datos de la cuenta.

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 lista es deliberadamente corta y deliberadamente mixta. cores, lang y dpr son campos de control: los aporta un perfil, así que deberían coincidir en ambos nodos y una discrepancia es lo primero que hay que investigar. renderer solo es un campo de control en el modo WebGL por defecto y pasa al lado del host con --bot-webgl=real, y por eso el registro de ejemplo que sigue lo deja fuera del grupo de control. pixels es un campo de host: dibuja un único triángulo fijo a pantalla completa cuyo fragment shader mezcla tres términos seno con un producto de fract, lee cada píxel de vuelta con readPixels y pliega el canal verde en un resumen FNV-1a corto. La interpolación y la cobertura de subpíxel son precisamente donde dos rasterizadores divergen, así que el shader tiene que variar sobre la superficie: un color constante, y un clear en particular, devuelve los mismos bytes en todos los backends y no mide nada. Dos condiciones de lanzamiento hacen que la medición signifique lo que afirma. La emulación de GPU tiene que estar desactivada y WebGL tiene que estar en modo real: los valores por defecto responden un dibujo WebGL desde la capa del perfil, que controla tanto la salida del shader como la lectura de la imagen, además de las cadenas de identidad, así que una ejecución que se queda en los valores por defecto mide el perfil y luego lo reporta como campo de host. El registro también lleva devicePixelRatio, porque el resumen depende de cuántos píxeles tiene la superficie; un nodo que renderiza la misma escena a otra escala produce un resumen distinto por una razón que no tiene nada que ver con su rasterizador. elapsed también es un campo de host, y uno burdo, así que separa la contención de la identidad sin pretender ser un benchmark.

Dos detalles de implementación cambian lo que significa el registro. El mapa de capacidades no contiene un node id, así que el script imprime el session id y la identidad del nodo se resuelve después desde el Grid; suponer que existe una capacidad nodeId produce None en todos los nodos y elimina en silencio el único campo que hace posible la atribución. Y la cadena de renderer solo está disponible cuando existe la extensión de depuración de renderer; un navegador que la oculta devuelve null, lo que es en sí mismo una señal de que el valor estaba enmascarado en lugar de reportado.

Capturar un registro por intento

El registro que hace visible la divergencia es pequeño: la identidad del nodo, la identidad del build y los dos grupos de campos. Un accesorio construido muestra cómo se ve una prueba en dos nodos que comparten un perfil.

// 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 salida construida es el punto, en dos partes. Todos los campos de control coinciden, porque ambos nodos cargaron el mismo perfil, mientras que el resumen de píxeles difiere, así que el veredicto es pixel_divergence y no un defecto de configuración. Las ramas impresas muestran después la regla que mantiene honesto el veredicto: un registro cuyos píxeles coinciden y cuyo tiempo transcurrido difiere se reporta como contention, no como una diferencia de renderizado entre nodos.

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

Tres reglas mantienen útil el registro. Lee los valores dentro de la sesión en lugar de leer las capacidades solicitadas por la prueba, porque la petición describe la intención y la página describe lo que llegó. Clasifica cada campo como control o host antes de comparar nada, porque las dos discrepancias tienen propietarios distintos. Y mantén la lista de campos corta y fija, para que dos registros se comparen mecánicamente en lugar de a ojo.

Atribuir un fallo a un nodo

La atribución es una cuestión de distribución. Recoge el nodeId de cada intento de la prueba que falla y cuenta cómo se reparten los fallos. Un fallo que aparece en un nodo y nunca en los demás respalda un artefacto de host, y el siguiente paso es comparar los campos de host de ese nodo con los de un nodo que pasa. Un fallo que aparece en todos los nodos a una tasa aproximadamente igual no es un problema de nodo, y el tiempo dedicado a comparar hosts es tiempo que no se dedica al código, al accesorio o a la aplicación.

Una segunda distribución importa igual. Cuando un nodo anuncia más slots de los que el host puede atender con comodidad, varias sesiones comparten una máquina y cada sesión ve más contención. Una prueba que solo falla cuando el nodo está al límite es un artefacto de planificación y no una diferencia de renderizado, y el registro los separa porque la contención mueve el campo de tiempo transcurrido mientras que una diferencia de rasterizador mueve el campo de píxeles.

La guía de integración de perfiles con Selenium cubre quién posee el lanzamiento, el directorio de datos de usuario y el cierre de sesión en un nodo; el análisis de esperas cubre la mitad temporal del mismo diagnóstico. El paso previo a ambos es decidir si la diferencia provino del host en absoluto.

Separar contención de divergencia

Un nodo que ejecuta cuatro sesiones a la vez no se comporta como un nodo que ejecuta una. El Distributor considera lleno un nodo cuando alcanza maxSessionCount, y por defecto un nodo anuncia un slot por cada CPU disponible para navegadores basados en Chromium y Firefox, así que un host de dieciséis núcleos puede aceptar dieciséis sesiones concurrentes salvo que la configuración indique otra cosa. Cada una de esas sesiones compite por el mismo ancho de banda de memoria, el mismo rasterizador y el mismo disco, y una prueba que mide cualquier cosa sobre el tiempo verá la contención antes que cualquier diferencia de identidad.

La distinción importa porque las dos formas de fallo se parecen en un informe y tienen correcciones distintas. Un fallo por contención aparece solo cuando el nodo está ocupado, se mueve entre pruebas en lugar de entre nodos y desaparece en una ejecución en serie. Un fallo por divergencia aparece siempre que un nodo concreto ejecuta la prueba, esté ocupado o inactivo, y se reproduce en una ejecución en serie en ese nodo. Registrar el número de sesiones concurrentes junto a la identidad del nodo es lo que los separa, porque ese número es la variable que cambió.

La contención es también la razón por la que contar nodos es un mal indicador de capacidad. Añadir nodos a un Grid aumenta el número de hosts disponibles, pero no cambia cuántas sesiones acepta cada host, y una flota escasa de memoria por sesión seguirá produciendo los mismos fallos por contención a mayor escala. El ajuste maxSessionCount y la memoria disponible por sesión son los dos números que deciden esto, y ambos pertenecen al registro del nodo en lugar de a la memoria de un equipo.

Hay una tercera forma que no es ni contención ni divergencia: una prueba que depende de datos que cambian con el tiempo. Un precio, una fecha o una longitud de lista que el accesorio asumió fijos fallarán con un calendario que no tiene nada que ver con el nodo que los ejecutó, y el registro lo muestra porque los intentos fallidos se reparten de forma uniforme entre nodos y entre niveles de carga. El modelo de atribución de cuatro causas trata ese caso como una causa propia exactamente por esta razón.

Qué fija un perfil y qué no puede fijar

La mitad de identidad del registro es para lo que sirve un perfil. BotBrowser documenta los valores de identidad que aporta un perfil y los aplica antes de que se ejecute cualquier script de página, lo que no le da control del rasterizador ni del reloj, así que en el modo WebGL por defecto las cadenas de vendor y renderer UNMASKED, el recuento de núcleos reportado, las dimensiones de pantalla, la lista de fuentes y la pila de idioma son iguales en cada host que carga el mismo perfil con los mismos flags de lanzamiento. Con --bot-webgl=real las cadenas siguen al host, la única fila que sale del grupo de control en ese modo. Eso es lo que hace significativa una comparación de campos de control, y es por lo que una discrepancia de control apunta a la configuración y no al hardware.

La otra mitad queda fuera de alcance por diseño. Un perfil no rasteriza el fotograma, así que los bytes que un backend de software escribe para un shader difieren entre backends incluso con un perfil idéntico. Tampoco planifica el trabajo, así que un paso fijo tarda más en un host más ocupado o más lento. La guía multiplataforma de BotBrowser indica que la portabilidad exige la misma versión de BotBrowser, el mismo archivo de perfil y los mismos flags de lanzamiento, y su guía de servidor headless documenta que un servidor sin GPU renderiza mediante uno de varios backends de software cuyas ventajas y desventajas difieren. Una suite que compara píxeles entre nodos tiene, por tanto, que registrar también el backend y el nodo, y una suite que mide latencia tiene que registrar también la carga.

FAQ

¿Por qué la misma prueba pasa en un nodo y falla en otro?

El Distributor asignó las dos ejecuciones a slots distintos. Compara los dos registros de nodo antes de tocar la prueba: primero los campos de control, porque una discrepancia ahí significa que los perfiles o los flags de lanzamiento diferían, y después los campos de host, porque una discrepancia ahí significa que dos máquinas renderizaron o cronometraron el mismo trabajo de forma distinta.

¿Puede un perfil hacer idénticos dos nodos de Grid?

No. Un perfil fija los valores de identidad que lee una página, lo que elimina una amplia clase de divergencias, pero el host sigue aportando el rasterizador, la planificación de CPU y el reloj. Unos píxeles idénticos requieren el mismo backend de renderizado además del mismo perfil, y unos tiempos idénticos requieren la misma carga.

¿Por qué puede coincidir la cadena de renderer mientras los píxeles difieren?

Porque las deciden capas distintas y modos de lanzamiento distintos. En el modo WebGL por defecto, la cadena de renderer es un valor de identidad que aporta el perfil, mientras que los píxeles son lo que calcula el rasterizador del host, así que una cadena coincidente y un resumen de píxeles distinto son el par esperado. La documentación del backend de GPU de BotBrowser afirma que en ese modo las cadenas UNMASKED siguen controladas por el perfil a través de una migración de backend mientras los bytes de píxeles cambian. Con --bot-webgl=real las cadenas siguen al host, así que en dos nodos con pilas WebGL distintas tanto la cadena como el resumen se mueven juntos.

¿Cuántos intentos hacen falta antes de calificar un fallo como específico de un nodo?

Los suficientes para que el fallo esté ausente en los demás nodos bajo la misma carga. Un único fallo en un nodo y un único pase en otro son una hipótesis; un patrón a través de intentos repetidos es una atribución.

Fuentes

Conclusión práctica

Un nodo que discrepa de otro también puede estar ejecutando otra compilación del navegador, que es una causa distinta con una solución distinta; Chrome for Testing: anclar una compilación, no una huella cubre cómo anclar y qué deja fuera un anclaje.

Registra el session id, la versión de navegador y driver, tres campos de control, un resumen de píxeles y un tiempo en cada intento de cada prueba de Grid. Compara primero los campos de control: una discrepancia ahí es un fallo de configuración, no un misterio de hardware. Compara después los campos de host: una discrepancia ahí es la diferencia que un perfil no puede eliminar, así que registra el backend, la carga y el nodo en lugar de cambiar el perfil. Trata un fallo que se agrupa en un nodo como respaldo de un artefacto de host y un fallo que se reparte como un problema de código o de datos.

#Selenium Grid#Automatización De Pruebas#Huella Del Navegador#Divergencia Entre Nodos

Ejecuta la misma comprobación en BotBrowser

Carga un perfil, mantén dentro del navegador las señales que acabas de leer y confirma el resultado en tu propia máquina antes del despliegue.