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

Nu när du förstår grunderna till k6 och prestanstestning , är det ' dags att sätta in kunskapen i praktiken.. Denna artikel följer dig genom att skriva riktiga tester, ,, integrera dem i CI, M SK5, CD-rör, ,, och använda profileringsverktyg för att identifiera och fixa prestansproblem.

Det här är Del 2 av en tvåpartserie på lasttestning med k6:

  • Del 1: Inleiding: Introduction till kM SK1 installering , testtyper, och varför kSSK4
  • Del 2 (den här artikelnM SK2: skrivande tester , CI

Om du inte läst del 1, börjar där för att förstå kM SK2 grunder , installering , och tester innan du dyker in i implementeringen

Att sätta upp din testmiljö

Innan man startar k6 tester , släpper ' upp MinimalBlog för att testa

1. kör MinimalBlog lokalt

Att använda demoprojektet:

cd Mostlylucid.MinimalBlog.Demo
dotnet run

Eller med Docker:

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

Varför Release Mode spelar roll för prestationens tester

För noggranna resultattestning, alltid bygga i Release mode .:

dotnet run --configuration Release

Debug vs Release-modi skillnader:

Aspect Debug Modus Release Modus
Optimeringar Deaktiverad Hela JIT-optimeringar aktiverade
Inlening Minimalt ♫ ♫ ♫ Aggressiv metod som visar ♫
Den döda koden Bevarad Avlägsnat
Debug-symboler Hela PDB , extra metadata Minimal eller ingen
Ägningar Debug.Assert() aktiv Kompilerad ut
Begränsningar Extra säkerhetskontroller
Typiska överhead ♫ 2-10 ♫ x långsammare ♫

Varför avbugsmodiet ger missledande resultat:

  1. Inga JIT-optimeringar: Kompilern bevarar kostrukturen för att lösa problem snarare än att optimera hastigheten
  2. Extra instrumentation: Debug-byggnader innefattar kod för brytpunkter , variable inspection , och stapelspår
  3. Inget inlining: metoderna är inte inlinjerade, ' inte ilinerad, ♫ ♫ , ♫ som lägger till anslutande överskott som kommer, ♪ ♫ inte existerar i produktionen,
  4. Ägningar körs: Debug.Assert() uttalande utföra, och lägga till kontroller på att produktionen inte har
  5. Skillnade minnesmönster: Debug allocations inkluderar padding och spårning som påverkar GC-beteendet

När använda avbugsmodiet för att testa:

  • Misslyckanden i att lösa problem: När tester misslyckas och du behöver detaljerade stapelspår
  • Utforskning av specifika begäran: Attack en debugger för att spåra en enda problematisk fråga
  • Verifiering av loginering: Se till att din login fångar rätt information under last
  • Initiell utveckling: När man skriver nya test skript och kontrollerar att de når rätt slutpunkter

Recommended workflow:

# 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

tumregler: Om du

2. Förbereda testerdata

Att skapa flera markerdown-poster för att testa med:

# 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. Create Test Scripts Directory

mkdir k6-tests
cd k6-tests

Nu är vi - ' - redo att skriva våra tester.

Test 1: Smogtest

Låt oss börja med en enkel röktest för att kontrollera grundläggande funktionalitet.

File: 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'),
  });
}

kör testet:

k6 run smoke-test.js

Oväntad utgång:

     ✓ 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

Test 2: Cache-Valideringstest

MinimalBlog's prestationer beror mycket på att kassera

File: 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']}`);
  });
}

kör testet:

k6 run cache-test.js

Den här testen visar dig

  1. Första uppmaningens tid ( när kaset är kallt)
  2. Följande inmatningstid (cache warmM SK1
  3. Bevakning av kas-kolor

Test 3: Ladetest

Låt oss nu testa

File: 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
}

kör testet:

k6 run load-test.js

Interpreterande resultat:

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

Nyckelmått att titta på:

  • http_req: 95 ste percentilresponstid | ( | skulle vara under tröskel |)
  • http_req_failde: Błędfrekvens ( skulle vara nära 0%)
  • http_reqs: Hela genomgången

Test 4: Stresstest

Låt oss hitta MinimalBlog, ' och brytpunkten.

File: 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 }),
  };
}

kör testet:

k6 run stress-test.js

Titta på:

  • När börjar reageringstid att försämras?
  • När VU räknas dyker upp fel
  • Kommer systemet att återhämta sig efter att lasten minskar?

Test 5: Spiketest

Testa plötsliga trafikstockningar ( som i Reddit eller Hacker News

File: 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,
  });
}

kör testet:

k6 run spike-test.js

Den här testet bekräftar:

  • Handlar systemet med plötslig belastning?
  • Finns det lämpliga hastighetsbegränsningsmekanismer?
  • Kommer den att återhämta sig gracefully?

Test 6: Soaktest

lång test för att upptäcka minnesmängder eller resurser uttömning

File: 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);
}

kör testet:

k6 run soak-test.js

Nyckel: övervaka systemresurser under vätsketester

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

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

Titta på:

  • Graduell minnesökning (memory leak
  • CPU-användningen kryper upp
  • Reaktionszeit nedbrytning över tid

Test 7: Realistisk User Journey

simulera det faktiska användarbeteendet:

File: 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),
  };
}

kör testet:

k6 run user-journey-test.js

Det här skapar en HTML-rapport user-journey-report.html

Advanced: Testing MetaWeblog API

Om du har MetaWeblog aktiverat

File: 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'),
  });
}

kör testet:

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

Att köra tester i CI/CD

Att använda k6 som ett kvalitetgrind

Den kraftfullaste tillämpningen av k6 i CI/CD är kvalitet-grind - failar automatiskt att bygga om prestationen minskar . Här' är hur man implementerar komplexa k-testning i GitHub-aktioner

GitHub actions: Grundversion

Låt's börja med en grundläggande arbetsström som kör på varje dragning och blockerar sammankoppling om tester misslyckas

Att skapa .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

Nyckliga egenskaper:

  • körs på varje PR
  • Skeplar provdata automatiskt
  • Vänter till att app är redo innan den testar
  • kör flera testtyper
  • Det går inte att bygga om tröskeln inte är uppfyllda
  • Laddar upp resultat som artefakter
  • Ser sammanfattningen i GitHubs gränssnitt
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-aktioner: Utveckling med PR-kommentar

Att skapa .github/workflows/k6-pr-comment.yml att lägga upp resultaten som PR kommentarer:

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

Den här arbetsgången:

  • Beiträger leder direkt till PR
  • Ser nyckelmått i en tabell
  • Tillhandahåller full utgång i expanderande sektioner
  • länkar till den fulla testrun
  • Visuella statusindikatorer

GitHub actions: Performance Regression Detection

Att skapa .github/workflows/k6-baseline.yml att upptäcka prestansregressioner:

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

Den här arbetsgången:

  • Testar både PR och huvudavdelningen
  • Vergleichar prestationsmått
  • Om prestationen minskar >20% misslyckas
  • Försämrar att kombinera långsamma kod
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

bästa metoder för CI/CD k6 Testing

  1. Starta snabbt, Tor tidigt

    • kör röktester först (30sM SK1
    • Bara köra fulla tester om rökning passerar
    • Det misslyckades snabbt att spara CI minuter
  2. Använd lämpliga tröskel

    // 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. Cache beroenden

    - name: Cache .NET packages
      uses: actions/cache@v4
      with:
        path: ~/.nuget/packages
        key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
    
  4. Samma protokoll för misslyckade tester

    - name: Upload App Logs
      if: failure()
      uses: actions/upload-artifact@v4
      with:
        name: app-logs
        path: Mostlylucid.MinimalBlog.Demo/app.log
    
  5. Använd miljön- specifika tröskel

    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
      },
    };
    

Att övervaka k6 tester med Grafana

För avancerad övervakning kan man skicka k6 mätningar till Grafana

1. installera Prometheus och Grafana

docker-kompose.yml för övervakning:

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. kör k6 med Prometheus utgång

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

3. Import kM SK1 Tavlansbord i Grafana

  1. Gå till Grafana på: http://localhost:3000
  2. Lägg till Prometheus datakälla
  3. Importera k6 brädan (IDM SK2 18030)

Interpreterar k6 Metriker

Nyckelmått för att förstå

Response Time Metrics

  • http_req: Totala inmatningstid
    • avg: Medelvärde av responsstid
    • min/max: snabbaste / lågaste respons
    • med: Median (50 fjärde procentil )
    • p(90): 90ste procentil
    • p(95): 95ste percentil (bra SLA-måttrikt
    • p(99): 99ste procentil (fångstviktiga

Bra värden för MinimalBlog:

  • p(95) M SK1 | 300 ms för kaserad innehåll
  • p(95) < M SK2ms för första sändningstid

Utputtmetrik

  • http_reqs: Totala begäran
  • http_req_rate: begärningar per sekund
  • iteration_djuur: tid för en komplett repetition

Frågmetrik

  • http_req_failde: Misslyckade begäran | (4 |xx |, |
  • kontroller: Bekräftelsepass

Målet : < | 1%

Dataöverföring

  • data_bekommen: laddade ner data
  • data_sent: laddade upp data

De bästa metoderna för att testa MinimalBlog

1. Test i isolering

Testa MinimalBlog separat från andra tjänster:

# Stop other services
docker-compose down

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

2. Använd realistiska data

Don' inte testa med tomma markerdown-filer . Använd realistisk innehåll

  • varierade utgångslängder (kurt , medel, långtM SK3
  • Skillnade markerdown-fenskaper (kodexblockar, bilder , listor
  • Flera kategorier
  • Distribution av post i världen

3. Testa kasbehavior tydligt

MinimalBlog's prestationer är beroende av att kassera

// 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. Monitorsystemresurser

kör dessa tillsammans med k6 tester

# 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. Se realistiska tröskel

inte sätta trösklar för strikta eller för lurade

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. Testa olika scenarior

// 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 dina tester

Håll tester i versionkontroll tillsammans med kod:

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

Att lösa vanliga problem

Utmaning 1: Verbinding avfärdad

Problemet:

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

Lösningen:

# 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

Problemet 2: hög felmängd

Problemet:

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

Lösningen:

  1. Prüfa applikationslogs för fel
  2. Verifiera att provdata existerar (markdown-filer)
  3. Reduera VU-tal (kan vara överbelad )
  4. Förhöja rampen, -, uppgången

Nummer 3: långsamma responsstid

Problemet:

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

Lösningen:

  1. Build in Release mode: dotnet run -c Release
  2. Bevestig att kaskning fungerar
  3. Kontrollera disken I/O (svärre lêer läserM SK2
  4. Profil med dotnet-trace:
dotnet-trace collect -p $(pgrep -f MinimalBlog) --format speedscope

Utmaning 4: minnesmängder i dämningstest

Problemet: minnesanvändningen växer kontinuerligt

Lösningen:

  1. Monitor med:
dotnet-counters monitor --counters System.Runtime[gc-heap-size,gc-committed,gc-allocated]
  1. Samma ett minnesdump:
dotnet-dump collect -p $(pgrep -f MinimalBlog)
dotnet-dump analyze memory.dump
  1. Prüfa för:
    • Kaset håller inte på att expira
    • File handleder inte håller på att stänga
    • Eventbehandlare håller inte på att avkolla

Verklig-World Example: Testing Before Release

Här är "'", en komplett testprocess innan MinimalBlogs Veröffentlichung .

#!/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!"

kör den:

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

Profilering med dotTrace och dotMemory

k6 säger till dig vad är långsamt, men profiler säger varför. kombinerar kM SK1 belastningstest med JetBrains profiler (dotTrace for performance, dotMemory for memoryMSC4 låter dig identifiera exakta bottlenecks i MinimalBlog-koden

Varför är profilen under laddad?

Profilering utan belastning missar ofta verkliga problem

  • Cold start vs warm cache: olika kodexvägar
  • Konkurent tillgång: Lock-kontentionM SK1 tävlingsvillkor
  • minnestryck: GC-beteende under last
  • Förbrukning av resurser: Anslutningspooler , file handleder

k6 skapar en realistisk belastning medan profilern fångar vad som händer inuti din app

Setup: Att installera JetBrains verktyg

Option 1: JetBrains dotTrace/dotMemory (Commercial

ladda ner från JetBrains:

# 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: dotnet - spåra ( fria, byggt, - i )

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

För den här guiden, ,, visar vi båda angreppssätten.

Profilering av arbetsflödet

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

steg 1: baseline utan last steg 2: Profil under k6 belastning steg 3: Vergleicha och identifiera bottlenecks steg 4: Fixera och återställ -test

Performanceprofilering med dotTrace

Scenario: Hitta långsamt Markdown-Parsing

1. Starta MinimalBlog med profiling

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. I ett annat terminalM SK1 köra k6 lasttestMSC3

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

3. Analisera profilen :

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

Vad ska man leta efter i rapportet:

  • Heißa vägar: Metoder som tar mest CPU-tid
  • Appellträd: Var tid spenderas i telefonhierarkin
  • Självtid: tid i metoden utom samtalen till andra metoder

Ett exempel på resultaten i 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

Det här visar

  • Markdown.Parse() är bottleneck #1
  • Regex matchning är #2 (80ms totaltM SK2
  • Den största delen av första 10 samtalen ( kastar kaset

Att använda dotnet

# 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/

Speedscope visualisering:

  • Flame-grafen visar uppmanade högar
  • Tidsorterad syn visar uitvoering över tid
  • Den vänstra stora bilden lyfter fram långsamma operationer

Profilering av minnen med dotMemory

Scenario: Detektor av minneslösch i kas

1. Starta MinimalBlog och anslut minnes profilern

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

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

2. köra kM SK1 lasttest :

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

3. Ta foton under och efter testet

# 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. Analisera minnesväxten

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

Vad att leta efter:

  • Kollektioner som växer: Lysar , Woordekat som aldrig minskar
  • Eventbehandlare: Otympliga händelser med referensramar
  • Cache-blöd: Cache växer bortom förväntade storlek
  • Stringaloktioner: Excessive string concatenation

exempel på resultaten:

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

Det här visar

  • Stringar växte med 10 MB ( potentiell strängläcka eller cacheproblem
  • BlogPosts kas är stabil (bra)
  • MarkdownDokument rensat av GC (goodM SK1

Att använda dotnet

# 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>

Kombinerad k6 + Profileringsskript

Här är ett komplettt skript som sätter igång k6 medan man profilar

File: 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 ""

kör den:

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

bästa metoder: k6 M SK2 Profilering

  1. Profilar alltid under realistisk belastning

    • Använd k6 för att simulera produktionstrafikmönster
    • Profile-idle-apps säger inget om verkliga prestationer
  2. Ta flera korta bilder

    • Innan lading (grundlinjen)
    • Under last (aktiv
    • Efter lading (kontrollera renning)
  3. Fokusera på varma vägar först

    • Rätta de största knorrarna först
    • 80/20 regel
  4. Messa före och efter

    • Always re-run k6 test efter optimering
    • Verifiera förbättring med profiler
  5. Titta på regressioner

    • Håll baseline-profiler i repo
    • Vergleicha profiler i CI/CD
  6. Profilutsläpp byggs

    • Debug-byggnader har olika egenskaper
    • Använd alltid -c Release för profilering

Tools Summary

verktyg syfte kostnad МSK3 bäst för
k6 Ladetestning Gratis Hitta vad som är långsamt
dotTrace Leistungsprofilering
dotMemory minnesprofilering betalad minneskredningarM SK3 GC-problem M
dotnet-trace Performance-profilering Gratis Grund CPU-profiling
dotnet-mätare Verklig -tids mätningar Gratis Livlig övervakning
dotnet-dump minnesdumpar gratis efteråt -mortemanalys
PerfView avancerad profilering gratis djupa Windowsprofilering

slutsats : Build Confidence Through Testing

Ladetestning med k6 ger dig självförtroendet att MinimalBlog kan hantera riktiga traffic.

  • Installera k6 på vilken plattform som helst.
  • Att skriva och köra olika typer av prestationerstest
  • Bevestig att kaskande beteende fungerar som väntat.
  • Hitta drivkraftsproblem innan de når produktionen
  • Integrera lasttest i CI/CD-rör
  • Att övervaka och tolka k6 mätningar

Ta med era nyckeln

  1. Börja med röktester - Verifiera grundläggande funktionalitet före djupare tester
  2. Testa kasching eksplicitt - MinimalBlog 's prestation beror på det
  3. Använd realistiska data - tomma filer inte representerar verkligt användande
  4. Monitora systemresurser - CPU
  5. Se meningsfulla trösklar - Baserat på faktiska krav
  6. Automatisera i CI/CD - Ta tag i regressioner tidigt
  7. Testa olika scenarior - Homepage , träffar, kategorierM SK3 fel

Vad vi lärde oss om MinimalBlog

Genom att testa k6 , kan vi kontrollera MinimalBlogs antaganden

  • Vinnig: p
  • Effektiv: låg minnesmängd , minimalt GC-tryck
  • Skalbar: Handler 100+ samtida användare på blygsam hårdvara
  • Tillförlitlig: < ≥0.1% felmängd under normal belastning
  • Cache-effektivt: 10x snabbare reaktioner med varm kass

Nästa steg

Nu vet du hur man testar ett ASP.NET med k6:

  1. Att skapa ett basnivå-prestentialprofil för din konfiguration
  2. Att sätta upp automatiserade tester i ditt CI/CD-rör
  3. Runna dämpa tester innan större releases
  4. Monitora produktionsmått och jämför med testresultat
  5. Itera : test | -> | optimera |-> | test igen

Kom ihåg prestanstestning är en engångsmässig aktivitet. När du lägger in innehåll , modifierar koden , eller ändrar hosting-providerna M SK3 omarbetar MSC4 gör de här testerna för att säkerställa att din app är snabb och tillförlitlig

resurser

k6 Dokumentation

GitHub-aktioner

Profileringsverktyg

Monitoring & Visualisering

MinimalBlog

ASP.NET Performance

lycklig testning!

logo

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