Back to "Load Testing ASP.NET Core Applications with k6: Practical Implementation"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

ASP.NET k6 Performance Testing

Load Testing ASP.NET Core Applications with k6: Practical Implementation

Tuesday, 02 December 2025

Maintenant que vous comprenez les fondements de k6 et d’essais de performance ,, il est temps de mettre ces connaissances en pratique M SK3 Cet article vous guide à l’élaboration de tests réels , leur intégration dans des pipelines de CI/CDM SK6 et l’utilisation d’outils de profilage pour identifier et corriger les lacunes du rendement MSC7

Il s'agit de Partie 2 d'une série de deux pièces - sur l'essai de charge avec kM SK1

  • Partie 1: Introduction: Introduction à l’installation de kM SK1 Types d’essai de , , et pourquoi k6
  • Partie 2: Essais de rédactionM SK1 CI/ Intégration à la CD , profilageMNK4 et exemples réelsMRK5world

Si vous n’avez pas lu la partie ', commencez par y comprendre les bases de k6 l’installationM SK3 , et les types d’essai avant de s’engager dans la mise en œuvre.

Établissement de votre environnement d'essai

Avant de lancer k6 testsM SK1 laissez's mettre en place MinimalBlog pour les essais :

1. Faire fonctionner MinimalBlog localement

Utiliser le projet de démonstration:

cd Mostlylucid.MinimalBlog.Demo
dotnet run

Ou en utilisant Docker:

docker run -d -p 5000:8080 \
  -v $(pwd)/Markdown:/app/Markdown \
  --name minimalblog-test \
  scottgal/minimalblog:latest

Pourquoi le mode de mise à jour est important pour l'essai du rendement

Pour des essais de performance exacts, bâtir toujours en mode de mise à jour:

dotnet run --configuration Release

Différences entre débugging et mode de diffusion:

Aspect Modèle de débugging
Optimisations désactivé Optimisations JIT complètes activées
Insérer Minimale Méthode agressive d'affichage de ≥
Code mort Préservé Éliminé
Symboles de débug Données métadonnées supplémentaires à la base de données PDB entière, Minimale ou aucune ≥
Affirmations Debug.Assert() Activité Compilé à l'extérieur
Contrôle des limites Contrôles de sécurité supplémentaires Optimisé à l’extérieur où il est sécuritaire
Coûts généraux 2-10x plus lentes MSC2 Performance de base MSC3

Pourquoi le mode de débug donne des résultats trompeurs:

  1. Aucune optimisation de JIT: Le compiler préserve la structure du code pour le débugging plutôt que pour l'optimisation de la vitesse
  2. Instrumentation supplémentaire: Les constructions de débugging comprennent le code pour les points de rupture , l'inspection des variables M SK2 et les traces du cheminement
  3. Aucune mise en relief: Les méthodes ne sont pas 't inlined, adding call overhead that wonM SK3t exist in production
  4. Exécution des affirmations: Debug.Assert() les énoncés exécutent, en ajoutant des vérifications que la production won' n’a pas
  5. Modèles de mémoire différents: Les allocations de débugging comprennent le padding et le suivi qui affectent le comportement du GC

Quand utiliser le mode de débug pour tester:

  • Défaillances dans la résolution des problèmes: Lorsque les essais échouent et que vous avez besoin de traces détaillées du cheminement
  • Enquête sur des demandes spécifiques: Attacher un débugger pour tracer une seule demande problématique
  • Vérifier l'enregistrement: Assurez-vous que votre enregistrement saisit l'information appropriée sous charge
  • Développement initial: Lorsque vous rédigez de nouveaux scripts d'essai et que vous vérifiez qu'ils atteignent les points finals appropriés

Flux de travail recommandé:

# 1. Develop and debug with Debug mode
dotnet run                           # Debug mode (default)

# 2. Validate functionality works
k6 run --vus 1 --duration 10s smoke-test.js

# 3. Switch to Release for actual performance testing
dotnet run -c Release

# 4. Run full load tests
k6 run load-test.js

Règle de la poignée: Si vous ' mesurez le rendement , utilisez la version de mise à jour

2. Préparer les données d'essai

Créer plusieurs postes de démarrage pour tester avec:

# Create test posts directory
mkdir -p TestMarkdown

# Create sample posts
for i in {1..10}; do
  cat > TestMarkdown/test-post-$i.md <<EOF
# Test Post $i




This is test post number $i with some **bold** text and *italic* text.

## Sample Content

- Item 1
- Item 2
- Item 3

\`\`\`csharp
public class Test {
    public int Value { get; set; }
}
\`\`\`
EOF
done

3. Créer un répertoire des scripts d'essai

mkdir k6-tests
cd k6-tests

Nous sommes maintenant prêts à rédiger nos tests.

Essai 1: Essai de fumée

Let's commencer par un simple test de fumée pour vérifier la fonctionnalité de base:

Dossier: smoke-test.js

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  vus: 1,
  duration: '30s',
  thresholds: {
    http_req_duration: ['p(95)<500'], // 95% of requests should be below 500ms
    http_req_failed: ['rate<0.01'],    // Less than 1% errors
  },
};

const BASE_URL = __ENV.BASE_URL || 'http://localhost:5000';

export default function() {
  // Test homepage
  let response = http.get(`${BASE_URL}/`);
  check(response, {
    'homepage status is 200': (r) => r.status === 200,
    'homepage has content': (r) => r.body.length > 0,
    'homepage response time OK': (r) => r.timings.duration < 500,
  });

  sleep(1);

  // Test a single post
  response = http.get(`${BASE_URL}/post/test-post-1`);
  check(response, {
    'post status is 200': (r) => r.status === 200,
    'post has content': (r) => r.body.length > 0,
    'post contains title': (r) => r.body.includes('Test Post 1'),
  });

  sleep(1);

  // Test categories page
  response = http.get(`${BASE_URL}/categories`);
  check(response, {
    'categories status is 200': (r) => r.status === 200,
    'categories has content': (r) => r.body.length > 0,
  });

  sleep(1);

  // Test category filter
  response = http.get(`${BASE_URL}/category/Testing`);
  check(response, {
    'category filter status is 200': (r) => r.status === 200,
    'category has posts': (r) => r.body.includes('Test Post'),
  });
}

Faire l’essai:

k6 run smoke-test.js

Résultats escomptés:

     ✓ homepage status is 200
     ✓ homepage has content
     ✓ homepage response time OK
     ✓ post status is 200
     ✓ post has content
     ✓ post contains title
     ✓ categories status is 200
     ✓ categories has content
     ✓ category filter status is 200
     ✓ category has posts

     checks.........................: 100.00% ✓ 300  ✗ 0
     data_received..................: 1.2 MB  40 kB/s
     data_sent......................: 15 kB   500 B/s
     http_req_duration..............: avg=45ms min=12ms med=38ms max=156ms p(95)=98ms
     http_reqs......................: 120     4/s

Essai de validation du cache 2:

MinimalBlog's performance dépend fortement du caching. LetM SK2s verify it works :

Dossier: cache-test.js

import http from 'k6/http';
import { check, group } from 'k6';

export const options = {
  vus: 1,
  iterations: 10,
};

const BASE_URL = __ENV.BASE_URL || 'http://localhost:5000';

export default function() {
  group('Memory Cache Test - First Request', () => {
    const start = Date.now();
    const response = http.get(`${BASE_URL}/post/test-post-1`);
    const duration = Date.now() - start;

    check(response, {
      'first request successful': (r) => r.status === 200,
    });

    console.log(`First request: ${duration}ms`);
  });

  group('Memory Cache Test - Cached Request', () => {
    const start = Date.now();
    const response = http.get(`${BASE_URL}/post/test-post-1`);
    const duration = Date.now() - start;

    check(response, {
      'cached request successful': (r) => r.status === 200,
      'cached request is faster': (r) => r.timings.duration < 100,
    });

    console.log(`Cached request: ${duration}ms`);
  });

  group('Output Cache Headers', () => {
    const response = http.get(`${BASE_URL}/post/test-post-1`);

    check(response, {
      'has cache headers': (r) => r.headers['Cache-Control'] !== undefined,
      'output cache working': (r) => {
        const age = r.headers['Age'];
        return age !== undefined && parseInt(age) >= 0;
      },
    });

    console.log(`Cache-Control: ${response.headers['Cache-Control']}`);
    console.log(`Age: ${response.headers['Age']}`);
  });
}

Faire l’essai:

k6 run cache-test.js

Ce test vous montrera:

  1. Premier temps de demande (lorsque le cache est froid)
  2. Temps de demande subséquents (cache warmM SK1
  3. Vérification des titres de cache

Essai 3: Essai de chargement

Maintenant, let's test under realistic load:

Dossier: load-test.js

import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';

// Custom metrics
const errorRate = new Rate('errors');

export const options = {
  stages: [
    { duration: '2m', target: 10 },   // Ramp up to 10 users
    { duration: '5m', target: 10 },   // Stay at 10 users
    { duration: '2m', target: 0 },    // Ramp down to 0
  ],
  thresholds: {
    http_req_duration: ['p(95)<300'],  // 95% under 300ms
    http_req_failed: ['rate<0.01'],     // Less than 1% errors
    errors: ['rate<0.1'],               // Less than 10% errors
  },
};

const BASE_URL = __ENV.BASE_URL || 'http://localhost:5000';

const scenarios = [
  { weight: 50, path: '/' },                    // 50% homepage
  { weight: 30, path: '/post/test-post-1' },    // 30% specific post
  { weight: 10, path: '/categories' },          // 10% categories
  { weight: 10, path: '/category/Testing' },    // 10% category filter
];

function weightedChoice(scenarios) {
  const total = scenarios.reduce((sum, s) => sum + s.weight, 0);
  let random = Math.random() * total;

  for (const scenario of scenarios) {
    random -= scenario.weight;
    if (random <= 0) return scenario;
  }

  return scenarios[0];
}

export default function() {
  const scenario = weightedChoice(scenarios);
  const response = http.get(`${BASE_URL}${scenario.path}`);

  const success = check(response, {
    'status is 200': (r) => r.status === 200,
    'response time OK': (r) => r.timings.duration < 500,
    'has content': (r) => r.body.length > 0,
  });

  errorRate.add(!success);

  sleep(Math.random() * 2 + 1); // Random sleep 1-3 seconds
}

Faire l’essai:

k6 run load-test.js

Interprétation des résultats:

scenarios: (100.00%) 1 scenario, 10 max VUs, 9m30s max duration
  default: 2m00s ramp-up, 5m00s plateau, 2m00s ramp-down

     ✓ status is 200
     ✓ response time OK
     ✓ has content

     checks.........................: 100.00% ✓ 3450  ✗ 0
     data_received..................: 12 MB   22 kB/s
     data_sent......................: 132 kB  244 B/s
     errors.........................: 0.00%   ✓ 0     ✗ 1150
     http_req_duration..............: avg=42ms min=8ms med=35ms max=245ms p(95)=86ms p(99)=156ms
     http_reqs......................: 1150    2.12/s
     iteration_duration.............: avg=2.1s min=1.0s med=2.0s max=3.4s
     vus............................: 10      min=0   max=10

Mesures clés à suivre:

  • http_reqM SK1duration p(95): 95premier temps de réponse pour le percentile.
  • http_req_faille: Taux d'erreur
  • http_reqs: Débit total

Test 4: Essai de stress

Let's trouver MinimalBlog's point de ruptureM SK2

Dossier: stress-test.js

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '2m', target: 10 },    // Normal load
    { duration: '2m', target: 50 },    // Increase to 50
    { duration: '2m', target: 100 },   // Stress at 100
    { duration: '2m', target: 200 },   // High stress at 200
    { duration: '3m', target: 0 },     // Recovery
  ],
  thresholds: {
    http_req_duration: ['p(99)<3000'], // 99% under 3s even under stress
  },
};

const BASE_URL = __ENV.BASE_URL || 'http://localhost:5000';

export default function() {
  const responses = http.batch([
    ['GET', `${BASE_URL}/`],
    ['GET', `${BASE_URL}/post/test-post-1`],
    ['GET', `${BASE_URL}/post/test-post-2`],
    ['GET', `${BASE_URL}/categories`],
  ]);

  responses.forEach((response, index) => {
    check(response, {
      [`request ${index} status is 200`]: (r) => r.status === 200,
    });
  });

  sleep(0.5);
}

export function handleSummary(data) {
  return {
    'stress-test-summary.json': JSON.stringify(data),
    stdout: textSummary(data, { indent: ' ', enableColors: true }),
  };
}

Faire l’essai:

k6 run stress-test.js

Vérification de:

  • Quand le temps de réponse commence à dégrader?
  • À quel compte VU apparaissent les erreurs?
  • Le système se rétablit-il après une diminution de la charge?

Essai 5: Essai d'étalonnage

Testez les pics de trafic soudainement (comme ceux qui sont affichés sur Reddit ou Hacker News):

Dossier: spike-test.js

import http from 'k6/http';
import { check } from 'k6';

export const options = {
  stages: [
    { duration: '1m', target: 10 },    // Normal traffic
    { duration: '30s', target: 200 },  // Sudden spike!
    { duration: '3m', target: 200 },   // Sustained spike
    { duration: '1m', target: 10 },    // Back to normal
    { duration: '1m', target: 0 },     // Ramp down
  ],
  thresholds: {
    http_req_duration: ['p(95)<1000'], // Allow higher latency during spike
    http_req_failed: ['rate<0.05'],     // Allow 5% errors during spike
  },
};

const BASE_URL = __ENV.BASE_URL || 'http://localhost:5000';

export default function() {
  const response = http.get(`${BASE_URL}/`);

  check(response, {
    'status is 200 or 503': (r) => r.status === 200 || r.status === 503,
  });
}

Faire l’essai:

k6 run spike-test.js

Cet essai vérifie:

  • Le système gère-t-il une charge soudaine?
  • Existe-t-il des mécanismes appropriés de limitation du taux?
  • Reprend-il gracieusement?

Essai 6: Soak Test

Long-essai en cours pour détecter les fuites de mémoire ou l'épuisement des ressources:

Dossier: soak-test.js

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '2m', target: 20 },    // Ramp up
    { duration: '3h', target: 20 },    // Stay at 20 for 3 hours
    { duration: '2m', target: 0 },     // Ramp down
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'],
    http_req_failed: ['rate<0.01'],
  },
};

const BASE_URL = __ENV.BASE_URL || 'http://localhost:5000';

export default function() {
  const response = http.get(`${BASE_URL}/post/test-post-${Math.floor(Math.random() * 10) + 1}`);

  check(response, {
    'status is 200': (r) => r.status === 200,
    'response time stable': (r) => r.timings.duration < 500,
  });

  sleep(2);
}

Faire l’essai:

k6 run soak-test.js

Importance: Surveiller les ressources du système au cours des essais de souche

# In another terminal
watch -n 5 'dotnet-counters ps | grep -i minimal'

# Or use top/htop
htop -p $(pgrep -f MinimalBlog)

Vérification de:

  • Augmentation graduelle de la mémoire (déversement de mémoire)
  • Utilisation du CPU s'accroît
  • Dégradation du temps de réponse au fil du temps

Essai 7: Voyage réaliste des utilisateurs

Simulation du comportement réel de l'utilisateur:

Dossier: user-journey-test.js

import http from 'k6/http';
import { check, group, sleep } from 'k6';
import { htmlReport } from 'https://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.js';

export const options = {
  vus: 10,
  duration: '5m',
  thresholds: {
    'group_duration{group:::01_homepage}': ['avg<500'],
    'group_duration{group:::02_browse_posts}': ['avg<500'],
    'group_duration{group:::03_categories}': ['avg<500'],
  },
};

const BASE_URL = __ENV.BASE_URL || 'http://localhost:5000';

export default function() {
  // Simulate a real user journey

  group('01_homepage', () => {
    const response = http.get(`${BASE_URL}/`);
    check(response, {
      'homepage loaded': (r) => r.status === 200,
      'homepage has posts': (r) => r.body.includes('Test Post'),
    });
    sleep(Math.random() * 3 + 2); // Read homepage 2-5 seconds
  });

  group('02_browse_posts', () => {
    // User clicks on a post
    let response = http.get(`${BASE_URL}/post/test-post-1`);
    check(response, {
      'post loaded': (r) => r.status === 200,
      'post has content': (r) => r.body.length > 500,
    });
    sleep(Math.random() * 20 + 10); // Read post 10-30 seconds

    // User clicks another post
    response = http.get(`${BASE_URL}/post/test-post-2`);
    check(response, {
      'second post loaded': (r) => r.status === 200,
    });
    sleep(Math.random() * 15 + 5); // Read second post 5-20 seconds
  });

  group('03_categories', () => {
    // User explores categories
    const response = http.get(`${BASE_URL}/categories`);
    check(response, {
      'categories loaded': (r) => r.status === 200,
    });
    sleep(2);

    // User clicks a category
    const categoryResponse = http.get(`${BASE_URL}/category/Testing`);
    check(categoryResponse, {
      'category posts loaded': (r) => r.status === 200,
      'category has posts': (r) => r.body.includes('Test Post'),
    });
    sleep(Math.random() * 10 + 5); // Browse category 5-15 seconds
  });
}

export function handleSummary(data) {
  return {
    'user-journey-report.html': htmlReport(data),
  };
}

Faire l’essai:

k6 run user-journey-test.js

Cela produit un rapport HTML: user-journey-report.html

Advanced: Testing MetaWeblog API

Si vous avez activé MetaWeblog, testez aussi:

Dossier: metaweblog-test.js

import http from 'k6/http';
import { check } from 'k6';
import encoding from 'k6/encoding';

export const options = {
  vus: 1,
  iterations: 10,
};

const BASE_URL = __ENV.BASE_URL || 'http://localhost:5000';
const USERNAME = __ENV.USERNAME || 'admin';
const PASSWORD = __ENV.PASSWORD || 'changeme';

function createXmlRpcRequest(methodName, params) {
  return `<?xml version="1.0"?>
<methodCall>
  <methodName>${methodName}</methodName>
  <params>
    ${params}
  </params>
</methodCall>`;
}

export default function() {
  // Test getRecentPosts
  const recentPostsXml = createXmlRpcRequest('blogger.getRecentPosts', `
    <param><value><string>0</string></value></param>
    <param><value><string>${USERNAME}</string></value></param>
    <param><value><string>${PASSWORD}</string></value></param>
    <param><value><int>10</int></value></param>
  `);

  const response = http.post(`${BASE_URL}/metaweblog`, recentPostsXml, {
    headers: { 'Content-Type': 'text/xml' },
  });

  check(response, {
    'MetaWeblog API responds': (r) => r.status === 200,
    'MetaWeblog returns XML': (r) => r.body.includes('<?xml'),
    'MetaWeblog has methodResponse': (r) => r.body.includes('methodResponse'),
  });
}

Faire l’essai:

k6 run -e USERNAME=admin -e PASSWORD=yourpassword metaweblog-test.js

Essais en cours dans CI/CD

Utilisation de k6 comme passerelle de qualité

L'utilisation la plus puissante de k6 dans CI/CD est une entrée de qualité - faillite automatique des constructions si la performance décroît . Voici' comment mettre en œuvre un test exhaustif kM SK3 dans les actions de GitHubMSC4

GitHub Actions: Verification de base

Let's commencer par un flux de travail de base qui fonctionne à chaque demande de retrait et bloque la fusion si les essais échouent

Créer .github/workflows/k6-pr-check.yml:

name: k6 Performance Check

on:
  pull_request:
    branches: [ main ]

jobs:
  performance-check:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '9.0.x'

      - name: Setup k6
        run: |
          sudo gpg -k
          sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg \
            --keyserver hkp://keyserver.ubuntu.com:80 \
            --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
          echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | \
            sudo tee /etc/apt/sources.list.d/k6.list
          sudo apt-get update
          sudo apt-get install k6

      - name: Create test data
        run: |
          mkdir -p TestMarkdown
          for i in {1..10}; do
            cat > TestMarkdown/test-post-$i.md <<EOF
          # Test Post $i
          
          

          This is test post $i with **bold** and *italic* text.

          \`\`\`csharp
          public class Test { public int Value { get; set; } }
          \`\`\`
          EOF
          done

      - name: Build MinimalBlog
        run: |
          cd Mostlylucid.MinimalBlog.Demo
          dotnet build --configuration Release

      - name: Start MinimalBlog
        run: |
          cd Mostlylucid.MinimalBlog.Demo
          dotnet run --configuration Release &
          echo $! > app.pid

          # Wait for app to be ready
          timeout 60 bash -c 'until curl -sf http://localhost:5000 > /dev/null; do
            echo "Waiting for app..."
            sleep 2
          done'

          echo "- App is ready!"

      - name: Run Smoke Test
        id: smoke-test
        run: |
          k6 run --out json=smoke-results.json k6-tests/smoke-test.js
          echo "smoke-test-passed=true" >> $GITHUB_OUTPUT

      - name: Run Load Test
        id: load-test
        run: |
          k6 run --out json=load-results.json k6-tests/load-test.js
          echo "load-test-passed=true" >> $GITHUB_OUTPUT

      - name: Run Cache Test
        id: cache-test
        run: |
          k6 run --out json=cache-results.json k6-tests/cache-test.js
          echo "cache-test-passed=true" >> $GITHUB_OUTPUT

      - name: Stop MinimalBlog
        if: always()
        run: |
          if [ -f Mostlylucid.MinimalBlog.Demo/app.pid ]; then
            kill $(cat Mostlylucid.MinimalBlog.Demo/app.pid) || true
          fi

      - name: Upload Test Results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: k6-test-results
          path: |
            *-results.json
            *.html
          retention-days: 30

      - name: Check Test Results
        if: always()
        run: |
          echo "## k6 Performance Test Results" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY

          if [ "${{ steps.smoke-test.outputs.smoke-test-passed }}" == "true" ]; then
            echo "- Smoke Test: PASSED" >> $GITHUB_STEP_SUMMARY
          else
            echo "- Smoke Test: FAILED" >> $GITHUB_STEP_SUMMARY
          fi

          if [ "${{ steps.load-test.outputs.load-test-passed }}" == "true" ]; then
            echo "- Load Test: PASSED" >> $GITHUB_STEP_SUMMARY
          else
            echo "- Load Test: FAILED" >> $GITHUB_STEP_SUMMARY
          fi

          if [ "${{ steps.cache-test.outputs.cache-test-passed }}" == "true" ]; then
            echo "- Cache Test: PASSED" >> $GITHUB_STEP_SUMMARY
          else
            echo "- Cache Test: FAILED" >> $GITHUB_STEP_SUMMARY
          fi

Faits clés:

  • Sur toutes les relations publiques
  • Créer les données d'essai automatiquement
  • Attende que l'application soit prête avant d'être testée
  • Exécute plusieurs types d'essai
  • Défaut de construire si les seuils ne sont pas atteints
  • Déposer les résultats en tant qu'artefacts
  • Indique le résumé dans l'interface de GitHub
sequenceDiagram
    participant PR as Pull Request
    participant GHA as GitHub Actions
    participant App as MinimalBlog
    participant k6 as k6 Tests

    PR->>GHA: Trigger workflow
    GHA->>GHA: Setup .NET & k6
    GHA->>GHA: Create test data
    GHA->>App: Build & Start
    App-->>GHA: App ready
    GHA->>k6: Run smoke test
    k6-->>GHA: Results
    GHA->>k6: Run load test
    k6-->>GHA: Results
    GHA->>k6: Run cache test
    k6-->>GHA: Results
    GHA->>App: Stop
    alt Tests Pass
        GHA->>PR: Mark as success
    else Tests Fail
        GHA->>PR: Block merge
    end

GitHub Actions: Amélioré avec les commentaires de la PR

Créer .github/workflows/k6-pr-comment.yml Poster les résultats comme commentaires publics:

name: k6 Performance with PR Comment

on:
  pull_request:
    branches: [ main ]

permissions:
  pull-requests: write
  contents: read

jobs:
  performance-test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '9.0.x'

      - name: Setup k6
        uses: grafana/setup-k6-action@v1

      - name: Create test data
        run: |
          mkdir -p TestMarkdown
          for i in {1..10}; do
            cat > TestMarkdown/test-post-$i.md <<EOF
          # Test Post $i
          
          
          Test content for post $i.
          EOF
          done

      - name: Build and Start App
        run: |
          cd Mostlylucid.MinimalBlog.Demo
          dotnet build -c Release
          dotnet run -c Release > app.log 2>&1 &
          APP_PID=$!
          echo $APP_PID > app.pid

          timeout 60 bash -c 'until curl -sf http://localhost:5000; do sleep 2; done'

      - name: Run k6 Tests
        id: k6-test
        run: |
          # Run tests and capture output
          k6 run --out json=results.json k6-tests/load-test.js > k6-output.txt 2>&1 || true

          # Parse results
          if grep -q "✓" k6-output.txt; then
            echo "tests-passed=true" >> $GITHUB_OUTPUT
          else
            echo "tests-passed=false" >> $GITHUB_OUTPUT
          fi

          # Extract key metrics
          P95=$(grep "http_req_duration" k6-output.txt | grep -oP 'p\(95\)=\K[0-9.]+' || echo "N/A")
          RPS=$(grep "http_reqs" k6-output.txt | grep -oP '\d+\.\d+/s' || echo "N/A")
          ERRORS=$(grep "http_req_failed" k6-output.txt | grep -oP '\d+\.\d+%' || echo "0%")

          echo "p95=${P95}" >> $GITHUB_OUTPUT
          echo "rps=${RPS}" >> $GITHUB_OUTPUT
          echo "errors=${ERRORS}" >> $GITHUB_OUTPUT

      - name: Comment PR
        uses: actions/github-script@v7
        if: always()
        with:
          script: |
            const fs = require('fs');
            const output = fs.readFileSync('k6-output.txt', 'utf8');

            const testsPassed = '${{ steps.k6-test.outputs.tests-passed }}' === 'true';
            const icon = testsPassed ? 'PASS' : 'FAIL';
            const status = testsPassed ? 'PASSED' : 'FAILED';

            const comment = `## ${icon} k6 Performance Test Results

            **Status:** ${status}

            ### Key Metrics
            | Metric | Value |
            |--------|-------|
            | P95 Response Time | ${{ steps.k6-test.outputs.p95 }}ms |
            | Requests/sec | ${{ steps.k6-test.outputs.rps }} |
            | Error Rate | ${{ steps.k6-test.outputs.errors }} |

            ### Thresholds
            - P95 < 300ms
            - Error rate < 1%

            <details>
            <summary>Full k6 Output</summary>


            \`\`\`
            ${output}
            \`\`\`

            </details>

            [View full test results](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
            `;

            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: comment
            });

      - name: Stop App
        if: always()
        run: |
          [ -f Mostlylucid.MinimalBlog.Demo/app.pid ] && \
            kill $(cat Mostlylucid.MinimalBlog.Demo/app.pid) || true

      - name: Upload Artifacts
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: k6-results
          path: |
            results.json
            k6-output.txt
            Mostlylucid.MinimalBlog.Demo/app.log

Ce flux de travail:

  • Postes résultant directement à la PR
  • Indique les mesures clés dans une table
  • Fournit une sortie complète dans la section élargible
  • Liens à l'essai complet
  • Indicateurs de situation visuelle

Actions de GitHub: Détection de la régression du rendement

Créer .github/workflows/k6-baseline.yml pour détecter les régressions de rendement:

name: Performance Regression Check

on:
  pull_request:
    branches: [ main ]

jobs:
  regression-check:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout PR code
        uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '9.0.x'

      - name: Setup k6
        uses: grafana/setup-k6-action@v1

      - name: Create test data
        run: |
          mkdir -p TestMarkdown
          for i in {1..20}; do
            echo "# Test Post $i" > TestMarkdown/test-$i.md
            echo "" >> TestMarkdown/test-$i.md
            echo '' >> TestMarkdown/test-$i.md
            echo "Test content $i" >> TestMarkdown/test-$i.md
          done

      - name: Test PR Branch
        run: |
          cd Mostlylucid.MinimalBlog.Demo
          dotnet build -c Release
          dotnet run -c Release &
          APP_PID=$!

          timeout 60 bash -c 'until curl -sf http://localhost:5000; do sleep 2; done'

          # Run test and save results
          k6 run --summary-export=pr-results.json k6-tests/load-test.js

          kill $APP_PID
          sleep 5

      - name: Checkout main branch
        uses: actions/checkout@v4
        with:
          ref: main
          path: baseline

      - name: Test Baseline (main branch)
        run: |
          cd baseline/Mostlylucid.MinimalBlog.Demo
          dotnet build -c Release
          dotnet run -c Release &
          APP_PID=$!

          timeout 60 bash -c 'until curl -sf http://localhost:5000; do sleep 2; done'

          # Run test and save results
          k6 run --summary-export=baseline-results.json ../k6-tests/load-test.js

          kill $APP_PID

      - name: Compare Results
        run: |
          # Extract P95 from both runs
          PR_P95=$(jq '.metrics.http_req_duration.values["p(95)"]' pr-results.json)
          BASE_P95=$(jq '.metrics.http_req_duration.values["p(95)"]' baseline/baseline-results.json)

          # Calculate percentage change
          CHANGE=$(echo "scale=2; (($PR_P95 - $BASE_P95) / $BASE_P95) * 100" | bc)

          echo "## Performance Comparison" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "| Branch | P95 Response Time |" >> $GITHUB_STEP_SUMMARY
          echo "|--------|-------------------|" >> $GITHUB_STEP_SUMMARY
          echo "| main (baseline) | ${BASE_P95}ms |" >> $GITHUB_STEP_SUMMARY
          echo "| PR | ${PR_P95}ms |" >> $GITHUB_STEP_SUMMARY
          echo "| Change | ${CHANGE}% |" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY

          # Fail if performance degrades by more than 20%
          if (( $(echo "$CHANGE > 20" | bc -l) )); then
            echo "- Performance degraded by ${CHANGE}%" >> $GITHUB_STEP_SUMMARY
            echo "::error::Performance regression detected: ${CHANGE}% slower than baseline"
            exit 1
          else
            echo "- Performance acceptable" >> $GITHUB_STEP_SUMMARY
          fi

Ce flux de travail:

  • Évaluer les relations avec la clientèle et le secteur principal
  • Comparaison des mesures de rendement
  • Défaut si la performance dégrade >20%
  • Prévention de la fusion du code lent
graph TD
    A[PR Submitted] --> B[Checkout PR Code]
    B --> C[Test PR Branch]
    C --> D[Checkout main Branch]
    D --> E[Test Baseline]
    E --> F{Calculate<br/>Difference}
    F -->|>20% Slower| G[FAIL Build]
    F -->|<20% Change| H[PASS Build]
    G --> I[Block Merge]
    H --> J[Allow Merge]

    style G stroke:#ff6b6b
    style H stroke:#51cf66

Pratiques exemplaires pour l'essai de CI/CD k6

  1. Démarrer rapidement, Porte tôt

    • Faire des essais de fumée d'abord (30sM SK1
    • Faire des essais complets seulement si la fumée passe
    • Défaut de sauvegarder rapidement les minutes CI
  2. Utiliser des seuils appropriés

    // Too strict for CI
    thresholds: { http_req_duration: ['p(95)<100'] }
    
    // Good for CI
    thresholds: {
      http_req_duration: ['p(95)<300', 'p(99)<1000'],
      http_req_failed: ['rate<0.01']
    }
    
  3. Dépendences de cache

    - name: Cache .NET packages
      uses: actions/cache@v4
      with:
        path: ~/.nuget/packages
        key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
    
  4. Recueillir les registres des tests qui ont échoué

    - name: Upload App Logs
      if: failure()
      uses: actions/upload-artifact@v4
      with:
        name: app-logs
        path: Mostlylucid.MinimalBlog.Demo/app.log
    
  5. Utiliser l'environnement-Crédits seuils

    const isPR = __ENV.GITHUB_EVENT_NAME === 'pull_request';
    
    export const options = {
      thresholds: {
        http_req_duration: isPR
          ? ['p(95)<500']  // Relaxed for PR
          : ['p(95)<300'], // Strict for main
      },
    };
    

Surveillance des essais k6 avec Grafana

Pour une surveillance plus poussée, vous pouvez transférer les métriques kM SK1 à Grafana:

1. Établissement Prometheus et Grafana

docker-composition.yml pour la surveillance:

version: '3.8'

services:
  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    depends_on:
      - prometheus

2. Exécuter k6 avec la sortie Prometheus

k6 run --out experimental-prometheus-rw load-test.js

3. Importer kM SK1 tableau de bord à Grafana

  1. Accès à Grafana : http://localhost:3000
  2. Ajouter la source de données Prometheus
  3. Importer k6 tableau de bord M SK1ID: 18030)

Interprétation de la métrique k6

Mesures clés à comprendre:

Mesures de temps d'intervention

  • http_req_duration: Durée totale de la demande
    • avg: Temps de réponse moyen
    • min/max:La réponse la plus rapideM SK1la réponse la moins lente
    • med: Median (50th percentileM SK2
    • p(90): 90th percentile
    • p(95): 95deuxième percentile (moyenne métrique de l'ALSM SK3
    • p(99): 99septième percentile

Bonnes valeurs pour MinimalBlog:

  • p(95) M SK1 ≥300ms pour le contenu caché
  • p(95) M SK1 ≥500ms pour les premières charges de temps -

Metriques de débit

  • http_reqs: Total des demandes
  • http_reqM SK1rate: Demandes par seconde
  • iteration_duration: Temps pour une itération complète

Mesures d'erreur

  • http_req_faille: Demandes défectueuses (4xx,
  • Les vérifications: Passe d'affirmation / Taux de défaillance

Taux d’erreur cible: M SK1 | 1%

Transfert de données

  • données_reçues: Données téléchargées
  • données_sent: Données mises à jour

Les meilleures pratiques pour tester MinimalBlog

1. Essai en isolation

Testez MinimalBlog séparément des autres services:

# Stop other services
docker-compose down

# Run only MinimalBlog
dotnet run --project Mostlylucid.MinimalBlog.Demo

2. Utiliser des données réalistes

Don' ne testez pas avec des fichiers de marquage videsM SK1 Utilisez un contenu réaliste:

  • Différentes longueurs de poste ( courte , moyenneM SK2 longue)
  • Différentes fonctions de marquage (blocs de code,imagesM SK2listes )
  • Plusieurs catégories
  • Réalisation de la distribution mondiale des postes

3. Modifier explicitement le comportement du cache d'essai

MinimalBlog's performance relies à la cache

// Test cold cache
export function setup() {
  // Clear cache by restarting app or waiting for expiration
}

// Test warm cache
export default function() {
  // First request (cache miss)
  http.get(`${BASE_URL}/post/test-1`);

  // Second request (cache hit)
  const cached = http.get(`${BASE_URL}/post/test-1`);
  check(cached, {
    'cache hit is faster': (r) => r.timings.duration < 50,
  });
}

4. Ressources du système de contrôle

Suivre ces essais à côté de k6 tests:

# CPU and memory
htop

# .NET metrics
dotnet-counters monitor -p $(pgrep -f MinimalBlog)

# GC collections
dotnet-counters monitor --counters System.Runtime[gc-heap-size,gen-0-gc-count,gen-1-gc-count,gen-2-gc-count]

5. Fixer des seuils réalistes

Ne pas fixer de seuils trop stricts ou trop souples:

export const options = {
  thresholds: {
    // Too strict (will fail unnecessarily)
    // http_req_duration: ['p(95)<50'],

    // Too loose (won't catch problems)
    // http_req_duration: ['p(95)<5000'],

    // Just right for MinimalBlog
    http_req_duration: ['p(95)<300', 'p(99)<1000'],
    http_req_failed: ['rate<0.01'],
    checks: ['rate>0.95'],
  },
};

6. Évaluer différents scénarios

// Homepage (list view)
http.get(`${BASE_URL}/`);

// Individual posts (detail view)
http.get(`${BASE_URL}/post/getting-started`);

// Categories (filtering)
http.get(`${BASE_URL}/category/Tutorial`);

// Non-existent posts (error handling)
http.get(`${BASE_URL}/post/does-not-exist`);

7. Version Vos tests

Garder les essais sous contrôle de version à côté du code:

Mostlylucid.MinimalBlog/
├── k6-tests/
│   ├── smoke-test.js
│   ├── load-test.js
│   ├── stress-test.js
│   ├── spike-test.js
│   ├── soak-test.js
│   ├── cache-test.js
│   └── user-journey-test.js
├── .github/
│   └── workflows/
│       └── k6-tests.yml
└── README-TESTING.md

Troubleshooting Common Issues

Numéro 1: Connection rejetée

Problème:

WARN[0000] Request Failed error="Get \"http://localhost:5000/\": dial tcp [::1]:5000: connect: connection refused"

Solution:

# Verify MinimalBlog is running
curl http://localhost:5000

# Check correct port
netstat -an | grep 5000

# Use correct URL
k6 run -e BASE_URL=http://localhost:5173 smoke-test.js

Numéro 2: Taux d'erreur élevé

Problème:

✗ http_req_failed: 45.23% (threshold is < 1%)

Solution:

  1. Vérifier les registres de l'application pour des erreurs
  2. Vérifier que les données d'essai existent (files de marquage)
  3. Réduire le nombre de VU (peut être surchargéM SK1
  4. Accroître le temps de rampe-up

Numéro 3: Temps de réponse lente

Problème:

http_req_duration..............: avg=2.5s p(95)=5.2s

Solution:

  1. Construire en mode de sortie: dotnet run -c Release
  2. Vérifier que le caching fonctionne
  3. Vérifier le disque I/O M SK1les lectures de fichier lentes)
  4. Profil avec dotnet-trace:
dotnet-trace collect -p $(pgrep -f MinimalBlog) --format speedscope

Numéro 4: fuites de mémoire dans les essais d'écoulement

Problème: L'utilisation de la mémoire augmente constamment

Solution:

  1. Surveillance avec:
dotnet-counters monitor --counters System.Runtime[gc-heap-size,gc-committed,gc-allocated]
  1. Collecte d’une décharge de mémoire:
dotnet-dump collect -p $(pgrep -f MinimalBlog)
dotnet-dump analyze memory.dump
  1. Vérifier pour:
    • Cache n'expire pas
    • Les handles du fichier ne s'arrêtent pas
    • Les gestionnaires d'événements ne s'en souscrivent pas

Examen réel-Exemple mondiale: Essais avant diffusion

Voici'un flux de travail d’essai complet avant la sortie du MinimalBlog:

#!/bin/bash
# test-release.sh

set -e

echo "Building: Building MinimalBlog in Release mode..."
dotnet build -c Release Mostlylucid.MinimalBlog/Mostlylucid.MinimalBlog.csproj

echo "Starting: Starting MinimalBlog Demo..."
dotnet run -c Release --project Mostlylucid.MinimalBlog.Demo &
APP_PID=$!

# Wait for app to start
echo "Waiting: Waiting for app to start..."
timeout 30 bash -c 'until curl -s http://localhost:5000 > /dev/null; do sleep 1; done'

echo "- App started successfully"

# Run test suite
echo "Running: Running smoke tests..."
k6 run k6-tests/smoke-test.js

echo "Running: Running cache validation..."
k6 run k6-tests/cache-test.js

echo "Running: Running load tests..."
k6 run k6-tests/load-test.js

echo "Running: Running stress tests..."
k6 run k6-tests/stress-test.js

echo "Running: Running spike tests..."
k6 run k6-tests/spike-test.js

echo "Generating: Generating combined report..."
# Process and combine results

echo "Stopping: Stopping app..."
kill $APP_PID

echo "- All tests completed successfully!"

Run it:

chmod +x test-release.sh
./test-release.sh

Profilage avec dotTrace et dotMemory

k6 vous dit Ce qui suit : est lente, mais les profileurs vous disent pourquoi. Combiner kM SK1 essais de charge avec les profileurs JetBrains (dotTrace for performance, dotMemory for memoryMSC4 vous permet d’identifier des goulets d’étranglement exacts dans le code MinimalBlog

Pourquoi le profil doit-il être chargé?

Le profilage sans charge manque souvent les problèmes réels-world issues:

  • Démarrage froid vs cache chaud: Voies de code différentes
  • Accès concurrent: Lock contentionM SK1 conditions de la course
  • Pression de mémoire: Le comportement du GC sous charge
  • Épuisement des ressources: Bases de connexionManages de fichier ,

k6 crée une charge réaliste alors que les profileurs captent ce qui se passe à l’intérieur de votre app.

Établissement: Installation des outils JetBrains

Option 1: JetBrains dotTraceM SK1dotMemory (Commercial)

Télécharger de JetBrains (en anglais seulement):

# Linux/Mac
wget https://download.jetbrains.com/resharper/dotUltimate.2024.3/JetBrains.dotTrace.GlobalTools.2024.3.nupkg

# Install as global tool
dotnet tool install --global JetBrains.dotTrace.GlobalTools
dotnet tool install --global JetBrains.dotMemory.Console

Option 2: dotnetM SK1trace (FreeMSC3 built-inMST5

dotnet tool install --global dotnet-trace
dotnet tool install --global dotnet-counters
dotnet tool install --global dotnet-dump

Pour ce guide, nous montrons les deux approches :

Profilage des flux de travail

flowchart LR
    A[Start App] --> B[Baseline<br/>Snapshot]
    B --> C[Start Profiler]
    C --> D[Run k6 Load Test]
    D --> E[Capture Metrics]
    E --> F[Stop Profiler]
    F --> G[Final Snapshot]
    G --> H{Analyze Results}
    H --> I[Identify<br/>Hotspots]
    I --> J[Optimize Code]
    J --> K[Re-test with k6]
    K --> L{Better?}
    L -->|Yes| M[Document & Deploy]
    L -->|No| I

    style M stroke:#51cf66
    style I stroke:#FFD700

Étape 1: Ligne de base sans charge Étape 2: Profil sous la charge k6 Étape 3: Comparer et identifier les lacunes Étape 4: Fixer et réexaminer

Profilage du rendement avec dotTrace

Scénario: Trouver une analyse du ralentissement de la marquage

1. Démarrer MinimalBlog avec le profilage attaché:

cd Mostlylucid.MinimalBlog.Demo

# Using dotTrace
dottrace attach <PID> --save-to=profile-baseline.dtt --timeout=60s

# Or build and run with profiling
dotnet build -c Release
dotnet run -c Release &
APP_PID=$!

# Attach profiler
dottrace attach $APP_PID --save-to=profile-load.dtt --timeout=120s

2. Dans un autre terminalM SK1 exécuter k 6 essai de chargement:

# This generates load while profiler captures data
k6 run --duration 60s --vus 20 k6-tests/load-test.js

3. Analyser le profil

# Convert to speedscope format for web viewing
dottrace report profile-load.dtt --output=report.html

Ce qu’il faut rechercher dans le rapport:

  • Routes chaudes: Méthodes consommant la plus grande partie du temps CPU
  • L'arbre d'appel: Où se passe le temps dans la hiérarchie des appels
  • Auto-temps: Temps dans la méthode à l'exclusion des appels aux autres méthodes

Exemples de constatations dans MinimalBlog:

Method                                    Total Time    Self Time    Calls
-----------------------------------------------------------------------
 MarkdownBlogService.GetAllPosts()         450ms        5ms          100
  ├─ LoadAllPosts()                       420ms        10ms         10
  │   ├─ ParseFile()                      400ms        20ms         100
  │   │   ├─ Markdown.Parse()             250ms        250ms        100   HOTSPOT
  │   │   └─ Regex.Match() (categories)   80ms         80ms         100   HOTSPOT
  │   └─ File.ReadAllText()               10ms         10ms         100
  └─ cache.GetOrCreate()                  25ms         25ms         90

Cela montre:

  • Markdown.Parse() est le créneau d’étanchéité #1
  • La correspondance avec le Regex est #2 (80ms totauxM SK2
  • La plupart des premières 10 appels, ( manquant de cache, puis en cachée

Utilisation du réseau de pointes-trace M SK1Alternative gratuite)

# Start tracing
dotnet-trace collect -p $APP_PID --format speedscope -o trace.json &
TRACE_PID=$!

# Run k6 test
k6 run --duration 30s --vus 10 k6-tests/load-test.js

# Stop tracing
kill -SIGINT $TRACE_PID

# View in browser
# Upload trace.json to https://www.speedscope.app/

Visualisation du spectre de vitesse:

  • Le graphique Flame montre les piles d'appel
  • La vue ordonnée du temps montre l'exécution au fil du temps
  • La vue lourde de gauche met en évidence les opérations lentes

Profilage de la mémoire avec dotMemory

Scénario: Détection des fuites de mémoire dans la cache

1. Démarrer MinimalBlog et attacher le profileur de mémoire:

cd Mostlylucid.MinimalBlog.Demo
dotnet run -c Release &
APP_PID=$!

# Take initial snapshot
dotmemory get-snapshot $APP_PID --save-to=snapshot-start.dmw

2. Faire l'essai de chargement kM SK1

# Run for 5 minutes with constant load
k6 run --duration 5m --vus 20 k6-tests/load-test.js

3. Faire des prises de vue pendant et après l'essai

# During load (after 2 minutes)
sleep 120
dotmemory get-snapshot $APP_PID --save-to=snapshot-load.dmw

# After load (cool down)
# Wait for k6 to finish, then wait 2 more minutes
sleep 120
dotmemory get-snapshot $APP_PID --save-to=snapshot-after.dmw

4. Analyser la croissance de la mémoire:

# Compare snapshots
dotmemory compare snapshot-start.dmw snapshot-after.dmw --save-to=comparison.html

Que chercher:

  • Collections en croissance: Listes, Dictionaries qui ne diminuent jamais
  • Gestionnaire des événements: Références relatives aux événements non abonnés
  • bloat de cache: Le cache augmente au-delà de la taille prévue
  • Répartition des chaînes: Concatenation de chaînes excessive

Exemple de constatations:

Object Type                      Start    Load     After    Growth
-------------------------------------------------------------------
System.String                    5 MB     45 MB    15 MB    +10 MB
BlogPost[]                       2 MB     2 MB     2 MB     0 MB    PASS
MarkdownDocument                 0 MB     8 MB     0 MB     0 MB    - (GC'd)
Dictionary<string, BlogPost>     1 MB     1 MB     1 MB     0 MB    PASS

Cela montre:

  • Les chaînes ont augmenté de 10 MB ( fuite potentielle des chaînes ou issue du cache)
  • Cache de BlogPost est stable (good)
  • MarkdownDocument nettoyé par GC (goodM SK1

Utilisation du réseau de pointe-dump M SK1Alternative gratuite)

# Capture memory dump during load
dotnet-dump collect -p $APP_PID -o dump-load.dmp

# After test
dotnet-dump collect -p $APP_PID -o dump-after.dmp

# Analyze dump
dotnet-dump analyze dump-load.dmp

> dumpheap -stat
> dumpheap -mt <MethodTable> -min 1000
> gcroot <address>

Script de profilage combiné k6 M SK1

Voici' est un script complet qui utilise k6 pendant le profilageM SK2

Dossier: profile-under-load.sh

#!/bin/bash
set -e

echo "Profiling: Profiling MinimalBlog under k6 load"

# Configuration
DURATION="120s"
VUS="20"
OUTPUT_DIR="profiling-results"

mkdir -p $OUTPUT_DIR

# Build and start app
echo "Building: Building MinimalBlog..."
cd Mostlylucid.MinimalBlog.Demo
dotnet build -c Release > /dev/null

echo "Starting: Starting MinimalBlog..."
dotnet run -c Release > $OUTPUT_DIR/app.log 2>&1 &
APP_PID=$!
echo $APP_PID > $OUTPUT_DIR/app.pid

# Wait for startup
timeout 60 bash -c "until curl -sf http://localhost:5000 > /dev/null; do sleep 2; done"
echo "- App started (PID: $APP_PID)"

# Baseline metrics
echo "Generating: Collecting baseline metrics..."
dotnet-counters collect -p $APP_PID --format json -o $OUTPUT_DIR/baseline-metrics.json &
COUNTER_PID=$!
sleep 10
kill -SIGINT $COUNTER_PID

# Take initial memory snapshot
echo "Snapshot: Initial memory snapshot..."
if command -v dotmemory &> /dev/null; then
    dotmemory get-snapshot $APP_PID --save-to=$OUTPUT_DIR/snapshot-start.dmw
else
    dotnet-dump collect -p $APP_PID -o $OUTPUT_DIR/dump-start.dmp
fi

# Start performance profiling
echo "Starting: Starting performance profiler..."
if command -v dottrace &> /dev/null; then
    dottrace attach $APP_PID --save-to=$OUTPUT_DIR/performance-profile.dtt --timeout=150s &
    PROFILER_PID=$!
else
    dotnet-trace collect -p $APP_PID --format speedscope -o $OUTPUT_DIR/trace.json &
    PROFILER_PID=$!
fi

sleep 5

# Run k6 load test
echo "Running: Running k6 load test ($DURATION, $VUS VUs)..."
cd ..
k6 run --duration $DURATION --vus $VUS \
    --out json=$OUTPUT_DIR/k6-results.json \
    k6-tests/load-test.js | tee $OUTPUT_DIR/k6-output.txt

echo "- k6 test completed"

# Wait for profiler to finish
wait $PROFILER_PID
echo "- Performance profile captured"

# Take final memory snapshot
echo "Snapshot: Final memory snapshot..."
if command -v dotmemory &> /dev/null; then
    dotmemory get-snapshot $APP_PID --save-to=$OUTPUT_DIR/snapshot-end.dmw

    echo "Comparing: Comparing memory snapshots..."
    dotmemory compare $OUTPUT_DIR/snapshot-start.dmw $OUTPUT_DIR/snapshot-end.dmw \
        --save-to=$OUTPUT_DIR/memory-comparison.html
else
    dotnet-dump collect -p $APP_PID -o $OUTPUT_DIR/dump-end.dmp
fi

# Stop app
echo "Stopping: Stopping MinimalBlog..."
kill $APP_PID

# Generate reports
echo "Generating: Generating reports..."

if command -v dottrace &> /dev/null; then
    dottrace report $OUTPUT_DIR/performance-profile.dtt \
        --output=$OUTPUT_DIR/performance-report.html
fi

# Summary
echo ""
echo "========================================="
echo "- Profiling Complete!"
echo "========================================="
echo ""
echo "Results in: $OUTPUT_DIR/"
echo ""
echo "Files generated:"
ls -lh $OUTPUT_DIR/
echo ""
echo "Generating: View results:"
echo "  - k6 output: cat $OUTPUT_DIR/k6-output.txt"
echo "  - Performance profile: open $OUTPUT_DIR/performance-report.html"
echo "  - Memory comparison: open $OUTPUT_DIR/memory-comparison.html"
echo "  - Speedscope trace: https://www.speedscope.app/ (upload trace.json)"
echo ""

Run it:

chmod +x profile-under-load.sh
./profile-under-load.sh

Pratiques exemplaires: k6 M SK2 Profilage

  1. Présenter toujours le profil sous une charge réaliste

    • Utiliser k6 pour simuler les patrons de trafic de production
    • Les applications inactives du profil ne vous disent rien sur la performance réelle
  2. Prendre des photos multiples

    • Avant la charge (baselineM SK1
    • Au cours de la charge (activeM SK1
    • Après charge (vérification de nettoyage)
  3. Mettre l'accent sur les pistes chaudes d'abord

    • Fixer les plus grands obstacles d'abord
    • 80/20 RègleM SK1 20% du code prend 80% de temps
  4. Mesure avant et après

    • Suivre toujours les essais de k6 après l'optimisation
    • Vérifier l'amélioration avec le profileur
  5. Surveiller les régressions

    • Garder les profils de base dans le répertoire
    • Comparer les profils dans CI/CD
  6. Ébauches de diffusion des profils

    • Les systèmes de débugging ont des caractéristiques de performance différentes
    • Utiliser toujours -c Release pour le profilage

Résumé des outils

Outil MSC1 But MSC2 Coût SMC3 Le meilleur pour SMC4
k6 Essais de chargement gratuits MSC2 Trouver ce qui est lente MSC3
La tracée des points Profilage du rendement Paiements MSC2 Détermination de la raison pour laquelle il est lente SMC3
Données de mémoire Profilage de la mémoire Paiements fuites de mémoire, Questions de GC MSC4
dotnet-trace Profilage de la performance Libre Profilation de base du CPU ≥
Coordonnateurs Réel- Mesures de temps Sans frais MSC3 Surveillance en direct MSC4
dotnet-dump Détournements de mémoire gratuits
PerfView Préparation de profils avancés Services gratuits

Conclusion: Building Confidence Through Testing

Les essais de chargement avec k6 vous donnent la confiance que MinimalBlog peut gérer le trafic réel.

  • Installer k6 sur n'importe quelle plate-forme (Windows, MacM SK3 LinuxMSC4
  • Rédiger et exécuter différents types d'essais de performance
  • Vérifier que le comportement de cache fonctionne comme prévu
  • Trouver des lacunes en matière de performance avant qu'elles ne se produisent
  • Intégrer les essais de charge dans les pipelines CI/CD
  • Surveiller et interpréter les métriques k6

Key Takeaways

  1. Commencer par les essais de fumée - Vérifier la fonctionnalité de base avant les essais plus approfondis
  2. Tester explicitement le caching - MinimalBlogThe performance of ' depends on it
  3. Utiliser des données réalistes - Les fichiers vides ne représentent pas l’utilisation réelle
  4. Surveillance des ressources du système - CPUM SK1 mémoire , GC au cours des essais
  5. Établir des seuils significatifs - Sur la base des besoins réels
  6. Automatiser dans CI/CD - Régressions précoces de capture
  7. Étudier différents scénarios - Page d'accueil

Ce que nous avons appris sur MinimalBlog

Par le biais de k6 essais, nous pouvons vérifier les revendications du MinimalBlogM SK2

  • Soutien rapide: pM SK1 < ≥300ms avec cache chaud
  • Efficacité: Utilisation faible de la mémoire , pression minimale du GC
  • Étalonnabilité: Appareils 100+ utilisateurs concurrents sur un matériel modeste
  • ReliableTaux d'erreur : < ≥0.1% sous charge normale
  • Cache- efficace: 10x réponses plus rapides avec cache chaud

Prochaines étapes

Maintenant que vous savez comment tester un ASP.NET avec k6:

  1. Créer un profil de performance de base pour votre configuration
  2. Mettre en place des essais automatisés dans votre pipeline CI/CD
  3. Faire des essais d'évacuation avant les principales mises à jour
  4. Surveiller les mesures de production et comparer celles des résultats d'essai
  5. Renouveler le test :, optimiser de nouveau le test

Remarque: Le test de performance n’est pas une activité ponctuelle.. Au fur et à mesure que vous ajoutez du contenuM SK1 modifiez le code , ou changez les fournisseurs d’hébergement, reMSC4suivez ces tests pour faire en sorte que votre application demeure rapide et fiable

Ressources

k6 Documentation

Actions de GitHub

Outils de profilage

Surveillance & Visualisation

MinimalBlog

ASP.Performance NET

Tests heureux!

logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.