2026-06-21 20:15:21 +08:00
|
|
|
|
// 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';
|
2026-06-23 09:57:04 +08:00
|
|
|
|
import 'dart:ui' show Offset, Rect;
|
2026-06-21 20:15:21 +08:00
|
|
|
|
|
|
|
|
|
|
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');
|
2026-06-23 09:57:04 +08:00
|
|
|
|
print('Size: $sizeKb KB (${outFile.lengthSync()} bytes)');
|
2026-06-21 20:15:21 +08:00
|
|
|
|
|
|
|
|
|
|
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(' ');
|
|
|
|
|
|
}
|