Files
BadNote/tool/gen_bench_pdf.dart
Akiba So d1b265dc70
Some checks failed
CI / Windows build (push) Has been cancelled
chore: clean analyzer to zero issues
Whole-project `flutter analyze` exited 1 on 17 pre-existing info/warning
lints (no errors) in dev tools and test files. Clean them so analyze is
green:
- editor_repository_test: drop the redundant sqflite_common import; keep
  the used utils import with a transitive-dep ignore.
- pen_* widget tests: `(_, __)` wildcard params -> `(_, _)`.
- search_snippet_test / gen_bench_pdf / gen_dense_strokes: drop needless
  interpolation braces; remove an unused `Size` show and an unused local;
  mark the gen tool as print-allowed.

No runtime behavior changed. flutter analyze: No issues found. Affected
tests: 22/22 pass.
2026-06-23 09:57:04 +08:00

240 lines
8.8 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// tool/gen_bench_pdf.dart
//
// Generates a benchmark PDF at test/assets/large_300p.pdf using
// package:syncfusion_flutter_pdf.
//
// syncfusion_flutter_pdf imports dart:ui which is only available inside the
// Flutter SDK runtime, so this file CANNOT be run via plain `dart run`.
//
// USAGE:
// flutter test tool/gen_bench_pdf.dart
// flutter test tool/gen_bench_pdf.dart --dart-define=PAGE_COUNT=50
//
// The script is structured as a flutter_test file (one `test(...)` block) so
// that `flutter test` invokes it with the full Flutter engine (dart:ui present).
// It is NOT a real unit test — it is a code-generation tool that happens to
// need the Flutter runtime. The test "passes" as long as the file is written
// successfully.
//
// Each page contains:
// - A bold title (page number heading)
// - Two paragraphs of body text
// - A ruled grid of lines (10x10)
// - A filled rectangle and an outlined ellipse
// This gives a realistic (non-blank) render load for pdfrx frame-timing tests.
// ignore_for_file: avoid_print
import 'dart:io';
import 'dart:math';
import 'dart:ui' show Offset, Rect;
import 'package:flutter_test/flutter_test.dart';
import 'package:syncfusion_flutter_pdf/pdf.dart';
void main() {
// Read PAGE_COUNT from --dart-define (default 300).
const int pageCount = int.fromEnvironment('PAGE_COUNT', defaultValue: 300);
test('generate test/assets/large_300p.pdf ($pageCount pages)', () {
final outputPath = _resolveOutputPath();
final outFile = File(outputPath);
outFile.parent.createSync(recursive: true);
final pdf = PdfDocument();
// Reusable fonts and brushes (created once, shared across pages).
final titleFont = PdfStandardFont(PdfFontFamily.helvetica, 18,
style: PdfFontStyle.bold);
final bodyFont = PdfStandardFont(PdfFontFamily.helvetica, 10);
final smallFont = PdfStandardFont(PdfFontFamily.helvetica, 8);
final blackBrush = PdfSolidBrush(PdfColor(0, 0, 0));
final darkBlueBrush = PdfSolidBrush(PdfColor(10, 30, 80));
final lightGrayBrush = PdfSolidBrush(PdfColor(220, 220, 220));
final accentBrush = PdfSolidBrush(PdfColor(60, 100, 200));
final gridPen = PdfPen(PdfColor(180, 180, 180), width: 0.3);
final borderPen = PdfPen(PdfColor(0, 0, 0), width: 1.0);
final accentPen = PdfPen(PdfColor(60, 100, 200), width: 1.5);
final rng = Random(42); // deterministic
for (int i = 1; i <= pageCount; i++) {
final page = pdf.pages.add();
final g = page.graphics;
final w = page.getClientSize().width;
final h = page.getClientSize().height;
// ── Title ──────────────────────────────────────────────────────────────
g.drawString(
'BadNote Benchmark — Page $i of $pageCount',
titleFont,
brush: darkBlueBrush,
bounds: Rect.fromLTWH(36, 30, w - 72, 28),
);
// Horizontal rule under title
g.drawLine(
PdfPen(PdfColor(60, 100, 200), width: 1.0),
Offset(36, 62),
Offset(w - 36, 62),
);
// ── Body text (two paragraphs) ────────────────────────────────────────
final paragraph1 =
'This page is part of a synthetic $pageCount-page benchmark PDF '
'generated by BadNote\'s tool/gen_bench_pdf.dart. Each page carries '
'non-trivial content (text, vector shapes, a line grid) to simulate '
'realistic rendering load for pdfrx frame-timing measurements. '
'Page index: $i. Seed value: ${rng.nextInt(99999)}.';
final paragraph2 =
'Performance target (MUST #4, §10/M1): pdfrx fling-scroll over '
'$pageCount pages in profile mode must stay at ≤ 16.6 ms median '
'frame time (build + raster) and ≤ 22 ms at p95, measured over '
'N ≥ 120 frames per §7.1 of the BadNote Phase 1 plan. If this gate '
'fails the backend choice is invalidated. Fill: ${_lorem(rng, 60)}.';
g.drawString(
paragraph1,
bodyFont,
brush: blackBrush,
bounds: Rect.fromLTWH(36, 72, w - 72, 80),
format: PdfStringFormat(lineSpacing: 4),
);
g.drawString(
paragraph2,
bodyFont,
brush: blackBrush,
bounds: Rect.fromLTWH(36, 158, w - 72, 80),
format: PdfStringFormat(lineSpacing: 4),
);
// ── 10×10 ruled grid ──────────────────────────────────────────────────
const gridLeft = 36.0;
const gridTop = 260.0;
final gridWidth = w - 72;
const gridHeight = 220.0;
const cols = 10;
const rows = 10;
final cellW = gridWidth / cols;
const cellH = gridHeight / rows;
for (int col = 0; col <= cols; col++) {
final x = gridLeft + col * cellW;
g.drawLine(
gridPen, Offset(x, gridTop), Offset(x, gridTop + gridHeight));
}
for (int row = 0; row <= rows; row++) {
const y = gridTop;
g.drawLine(gridPen, Offset(gridLeft, y + row * cellH),
Offset(gridLeft + gridWidth, y + row * cellH));
}
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) {
if ((row + col) % 3 == 0) {
g.drawRectangle(
brush: lightGrayBrush,
bounds: Rect.fromLTWH(
gridLeft + col * cellW + 0.5,
gridTop + row * cellH + 0.5,
cellW - 1,
cellH - 1,
),
);
}
}
}
g.drawRectangle(
pen: borderPen,
bounds: Rect.fromLTWH(gridLeft, gridTop, gridWidth, gridHeight),
);
for (int row = 0; row < rows; row++) {
g.drawString(
'R${row + 1}',
smallFont,
brush: blackBrush,
bounds: Rect.fromLTWH(
gridLeft + 2,
gridTop + row * cellH + 2,
cellW - 4,
cellH - 4,
),
);
}
// ── Accent shapes ──────────────────────────────────────────────────────
const shapeTop = gridTop + gridHeight + 18;
final rectW = 60.0 + (i % 8) * 10.0;
g.drawRectangle(
pen: accentPen,
brush: accentBrush,
bounds: Rect.fromLTWH(36, shapeTop, rectW, 24),
);
g.drawString(
'Page $i',
smallFont,
brush: PdfSolidBrush(PdfColor(255, 255, 255)),
bounds: Rect.fromLTWH(40, shapeTop + 6, rectW - 8, 14),
);
g.drawEllipse(
Rect.fromLTWH(36 + rectW + 16, shapeTop, 80, 24),
pen: accentPen,
);
// ── Footer ────────────────────────────────────────────────────────────
g.drawString(
'BadNote bench PDF • page $i/$pageCount • tool/gen_bench_pdf.dart',
smallFont,
brush: PdfSolidBrush(PdfColor(140, 140, 140)),
bounds: Rect.fromLTWH(36, h - 28, w - 72, 18),
format: PdfStringFormat(alignment: PdfTextAlignment.center),
);
}
final bytes = pdf.saveSync();
pdf.dispose();
outFile.writeAsBytesSync(bytes);
final sizeKb = (outFile.lengthSync() / 1024).toStringAsFixed(1);
print('Generated: $outputPath');
print('Pages: $pageCount');
print('Size: $sizeKb KB (${outFile.lengthSync()} bytes)');
expect(outFile.existsSync(), isTrue);
expect(outFile.lengthSync(), greaterThan(1024),
reason: 'PDF must be at least 1 KB');
}, timeout: const Timeout(Duration(minutes: 5)));
}
/// Resolves test/assets/large_300p.pdf relative to this script's location.
/// tool/gen_bench_pdf.dart → project root → test/assets/large_300p.pdf
String _resolveOutputPath() {
// When run via `flutter test`, the CWD is the project root.
return 'test/assets/large_300p.pdf';
}
/// Generates a deterministic Lorem-Ipsum-style filler of roughly [words] words.
String _lorem(Random rng, int words) {
const vocab = [
'lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur',
'adipiscing', 'elit', 'sed', 'eiusmod', 'tempor', 'incididunt',
'labore', 'dolore', 'magna', 'aliqua', 'enim', 'minim', 'veniam',
'quis', 'nostrud', 'exercitation', 'ullamco', 'laboris', 'nisi',
'aliquip', 'commodo', 'consequat', 'duis', 'aute', 'irure',
'reprehenderit', 'voluptate', 'velit', 'esse', 'cillum', 'fugiat',
'nulla', 'pariatur', 'excepteur', 'sint', 'occaecat', 'cupidatat',
'proident', 'culpa', 'officia', 'deserunt', 'mollit', 'anim',
];
return List.generate(words, (_) => vocab[rng.nextInt(vocab.length)])
.join(' ');
}