Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Lesedauer 6 Min.

Laufzeitgewinn messen

Mehr Shards können das Feedback beschleunigen, aber auch zugleich die CI-Kosten erhöhen. Ein Vergleich mit zwei Referenzläufen zeigt, wo sich zusätzliche Runner lohnen. Ebenso wichtig ist der Nachweis, dass weiterhin alle vorgesehenen Tests laufen.
© EMGenie

Der vorangegangene Teil 3 dieser Serie zum Thema „Schnellere .NET-CI-Pipelines mit Test Sharding“ hat die GitLab-Pipeline samt JUnit-Anzeige und getrennten Testdaten eingerichtet. Damit steht die technische Ausführung. Nun folgt die Entscheidung über die Shard-Anzahl: Wie viel früher liegt das vollständige Ergebnis vor, und welchen Ressourcenverbrauch verursacht die Beschleunigung?

Feedbackzeit und Runner-Minuten trennen

Die Feedbackzeit reicht vom Pipeline-Trigger bis zum vollständigen relevanten Ergebnis. Runner-Minuten summieren dagegen die Ausführungszeiten aller Jobs, ohne Warteschlangenzeit. Vier gleichzeitige Zehn-Minuten-Jobs belegen vierzig Runner-Minuten, das Ergebnis dieser Stufe liegt aber nach etwa zehn Minuten vor. Die Abrechnung kann davon abweichen, etwa durch Rundung, Freikontingente oder von Runnern unabhängige Kostenfaktoren.

Den kritischen Pfad erfassen

Die Teststufe endet mit dem zuletzt fertigen Shard, einschließlich Warteschlange, Vorbereitung, Discovery, Tests und Reports. Ein gemeinsamer Build davor und eine Ergebnisprüfung danach verlängern den kritischen Pfad: die Abfolge abhängiger Schritte, welche die Gesamtdauer bestimmt. Knappe Runner-Kapazität erzeugt weitere Wartezeiten durch mehrere Jobwellen.

Dauert der Build vier Minuten und der Testlauf zwanzig, benötigen selbst vier ideale Shards mindestens vier plus fünf Minuten – noch ohne Zusatzaufwand wie die Discovery, die je Shard erneut anfällt. Ein zehnminütiger Test setzt zudem eine Untergrenze für seinen Shard: Das Verfahren zerlegt dessen Testkörper nicht.

Rechenintensive Tests können von zusätzlichen CPU-Kernen auf weiteren Runnern profitieren. Bei I/O-Wartezeiten lohnt zuerst die interne Parallelisierung: Sie überlappt Wartephasen ohne zusätzliche Jobs. Sharding kann auch hier helfen, sofern Datenbank oder API die zusätzliche Last bewältigen.

Zwei Referenzen statt eines Vorherwerts

Zunächst läuft dieselbe Suite mit dem bisherigen dotnet test-Aufruf. Danach folgt ShardedTest mit genau einem Shard. Dieser zweite Referenzlauf zeigt, welchen Zusatzaufwand Discovery und Filterung verursachen. Anschließend vergleichen wir zwei, vier und gegebenenfalls acht Shards. Die interne Framework-Parallelisierung bleibt zunächst konstant.

Commit, Testdaten, SDK-Patch, Paketversionen und Runner-Klasse bleiben gleich. Auch Coverage, also die Erfassung der Codeabdeckung, muss in allen Varianten gleich konfiguriert sein. Warme Caches mit bereits geladenen Paketen und kalte Caches bilden getrennte Messgruppen. Pro Job notieren wir Wartezeit, Vorbereitung, Dauer des ShardedTest-Aufrufs, Reportverarbeitung und Gesamtdauer. Detaillierte Logs trennen innerhalb des Aufrufs Discovery und Testausführung.

Für einen ersten Vergleich eignen sich zehn bis zwanzig Wiederholungen pro Variante in wechselnder Reihenfolge. Das begrenzt Einflüsse durch Tageszeit und Infrastruktur. Das Protokoll enthält Median, Streuung und Ausreißer. Das 95. Perzentil (p95), unter dem 95 Prozent der Laufzeiten liegen, bleibt bei so wenigen Läufen instabil: Seltene Verzögerungen brauchen mehr Beobachtungen.

Ein Rechenbeispiel richtig lesen

Die Zahlen der nachstehenden Tabelle kommen aus einem Projekt. Die Teststufe enthält Vorbereitung und Reports. Runner-Minuten summieren die Jobdauern; der langsamste Job mal Shard-Anzahl wäre hierfür falsch.

Messungen verschiedener Testläufe mit einer variierenden Shard-Anzahl
 
VarianteTeststufe in MinutenRunner-Minuten
Bisheriger Lauf26,026,0
Ein Shard26,526,5
Zwei Shards15,029,0
Vier Shards10,034,0
Acht Shards8,049,0

Gegenüber dem bisherigen Lauf verkürzen vier Shards die Teststufe hier um rund 62 Prozent: (26 – 10) / 26. Die Runner-Zeit steigt um rund 31 Prozent: (34 − 26) / 26. Acht Shards sparen gegenüber vier Shards nur zwei weitere Minuten ein, benötigen aber 15 zusätzliche Runner-Minuten. Ob sich das lohnt, hängt von der Häufigkeit der Pipeline und dem Wert des früheren Feedbacks ab.

Die Kostenrechnung verwendet abgerechnete Minuten, Kostenfaktoren und Speicherpreise. Eigene Runner verursachen auch Betriebs- und Leerlaufkosten. Eingesparte Wartezeit ist nicht automatisch auch eingesparte Arbeitszeit. Relevant ist, ob späte Rückmeldungen Arbeitswechsel, blockierte Reviews oder verzögerte Releases verursachen.

Ein Ungleichgewicht sichtbar machen

Gleich viele Tests pro Shard bedeuten keine gleich langen Jobs. ShardedTest 1.1.0 verteilt die sortierte Liste nach Position modulo Shard-Anzahl. Es liest keine historischen Testdauern ein und verschiebt nach dem Start keine Arbeit von einem langsamen zu einem bereits fertigen Runner.

Ein einfaches Maß ist die längste gemessene Shard-Testdauer geteilt durch den Mittelwert aller Shard-Testdauern; Vorbereitung und Warteschlange bleiben außen vor. Eins beschreibt Gleichstand; zehn Minuten Maximum bei fünf Minuten Mittelwert ergeben den Faktor zwei. Größere Werte lenken die Suche auf lange Tests oder teure Fixtures. Sind wenige Fälle dauerhaft sehr teuer, lohnen sich deren Optimierung oder eine eigene Testgruppe vor weiteren Shards.

Das folgende Python-3-Beispiel verarbeitet die Ergebnisse der Shards:

 

from collections import defaultdict
from pathlib import Path
import xml.etree.ElementTree as ET
totals = defaultdict(lambda: [0, 0.0])
ns = {"t": "http://microsoft.com/schemas/VisualStudio/TeamTest/2010"}
for path in Path("results").glob("*/*.trx"):
    shard = path.parent.name
    root = ET.parse(path).getroot()
    for test in root.findall(".//t:UnitTestResult", ns):
        totals[shard][0] += 1        duration = test.get("duration")
        if not duration:
            continue
        hours, minutes, seconds = duration.split(":")        days, _, hours = hours.rpartition(".")
        totals[shard][1] += (
            int(days or 0) * 86400 + int(hours) * 3600            + int(minutes) * 60 + float(seconds)
        )
for shard, (count, seconds) in sorted(totals.items()):
    print(shard, count, round(seconds, 2))

 

Das Python-Script summiert zusätzlich die in TRX gespeicherten Einzelfallzeiten. Dazu liegen ausschließlich Reports eines Vergleichslaufs unter results/<shard>/*.trx, unabhängig vom verwendeten CI-Anbieter. Das Skript gruppiert sie nach Shard. Parallel laufende Tests überlappen sich jedoch: Ihre Zeitsumme ist keine Jobdauer und ersetzt nicht das vorherige Ungleichgewichtsmaß.

defaultdict verwaltet Zähler und Zeitsumme je Shard. glob findet die TRX-Dateien; path.parent.name liefert die Shard-Nummer. ns benennt den XML-Namensraum für die Suche nach UnitTestResult. Das Skript zählt diese Einträge und rechnet vorhandene duration-Werte in Sekunden um, einschließlich eines optionalen Tagesanteils. Es gibt Shard-Nummer, Ergebniszahl und Zeitsumme aus. Zwei parallele Zehn-Sekunden-Tests liefern zwanzig Fallsekunden bei etwa zehn Sekunden verstrichener Zeit.

Vollständigkeit als eigene Prüfung

Vor dem produktiven Umschalten vergleichen wir Auswahl und Ausführung mit einem Referenzlauf ohne Sharding. Dafür dient ein fehlerfrei abgeschlossener Lauf desselben Commits mit identischer Testkonfiguration. Zusammen müssen die Shards alle erwarteten Fälle enthalten; kein Fall darf zwei Shards angehören. Als Identität zählen Projekt, Ziel-Framework und Testfall einschließlich Datensatz. Gesamtzahlen reichen nicht: Ein fehlender und ein doppelter Test können sich numerisch ausgleichen. Übersprungene Tests erfassen wir getrennt von ausgeführten Tests.

Besondere Aufmerksamkeit verdienen Anzeigenamen. Das Tool bildet seine Filter aus DisplayName, nicht aus einer global eindeutigen, um Projekt und Ziel-Framework erweiterten Identität. Bei gleichen Anzeigenamen in mehreren Modulen lässt sich aus einem Namensfilter keine eindeutige Zuordnung ableiten. Deshalb beginnen die Beispiele bewusst mit einem einzelnen Projekt und einem Ziel-Framework.

Auch Exitcode null allein genügt nicht. Ein leer ausgewählter Shard endet in Version 1.1.0 erfolgreich. Bei mehreren MTP-Modulen ergänzt das Tool gegebenenfalls --ignore-exit-code 8, damit ein Modul ohne passende Tests den Shard nicht scheitern lässt. Die Discovery akzeptiert zudem einen Fehlercode, sofern sie bereits Tests parsen konnte. Deshalb prüfen wir Sollbestand, Auswahl und Berichte getrennt. Das Python-Listing liefert nur eine erste Übersicht, keinen Identitätsvergleich.

Die Shard-Anzahl folgt dem Messergebnis

Im Betrieb gehören fehlende Reports und sporadische Testfehler neben der Laufzeit ins Monitoring. Wiederholte Läufe dürfen den ursprünglichen Fehlschlag nicht verdecken. Neue Fehler nach der Aufteilung können auf geteilten Zustand oder überlastete Dienste hinweisen.

Zwei oder vier Shards mit geprüftem Testumfang bilden einen sinnvollen Einstieg. Weitere Jobs lohnen sich, solange frühere Rückmeldungen ihren zusätzlichen Ressourcenverbrauch rechtfertigen. Stagniert der Gewinn, geraten langsame Tests, Fixtures und Discovery in den Fokus. Mit wachsender Suite braucht es eine neue Messung.

Neueste Beiträge

KI in Anwendungen mit Microsoft Graph - Connected Apps mit .NET, Teil 4
Eine .NET-App verbindet Microsoft-365-Daten mit semantischer Suche und einem Sprachmodell und erzeugt daraus kontextbezogene Antworten.
9 Minuten
Lern! Dich! Schlau! - .NET Developer Conference 2026
Neu erworbenes Wissen, Austausch in den Gesprächen und hervorragendes Essen: Die DDC bietet Teilnehmenden ein Komplettpaket für die Fortbildung in .NET und KI.
6 Minuten
14. Okt 2026
GitLab CI mit Testreports einrichten - Schnellere .NET-CI-Pipelines mit Test Sharding, Teil 3
GitLab liefert die Shard-Nummern direkt an Meziantou.ShardedTest. Für aussagekräftige Ergebnisse im Merge Request braucht es zusätzlich passende Reports. Bei Integrationstests entscheidet die Trennung der Testdaten über zuverlässige Parallelläufe.
6 Minuten

Das könnte Dich auch interessieren

Tests gezielt verteilen - Schnellere .NET-CI-Pipelines mit Test Sharding, Teil 1
Lange Testsuiten verzögern das Feedback im Pull Request. Meziantou.ShardedTest verteilt die Testfälle auf mehrere CI-Jobs und verspricht dadurch mehr Geschwindigkeit. Dieser Einstieg zeigt, wie die Aufteilung funktioniert und welche Voraussetzungen nötig sind.
6 Minuten
GitHub Actions umstellen - Schnellere .NET-CI-Pipelines mit Test Sharding, Teil 2
Eine Jobmatrix bringt die lokale Testaufteilung auf mehrere Runner. Damit daraus ein verlässlicher CI-Lauf entsteht, müssen Shard-Nummern, Fehlerstatus und Ergebnisdateien zusammenpassen. Der Workflow zeigt die nötigen Schritte.
6 Minuten
GitLab CI mit Testreports einrichten - Schnellere .NET-CI-Pipelines mit Test Sharding, Teil 3
GitLab liefert die Shard-Nummern direkt an Meziantou.ShardedTest. Für aussagekräftige Ergebnisse im Merge Request braucht es zusätzlich passende Reports. Bei Integrationstests entscheidet die Trennung der Testdaten über zuverlässige Parallelläufe.
6 Minuten
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige