Files
BadNote/tool/gen_bench_pdf.dart

240 lines
8.8 KiB
Dart
Raw Normal View History

// 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, Size;
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(' ');
}