@reekon-tools/react-native-pdf-canvas 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +57 -0
- package/PdfCanvas.podspec +68 -39
- package/README.md +213 -1316
- package/android/build.gradle +67 -54
- package/android/consumer-rules.pro +7 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasNativeTest.java +254 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/TestPdfs.java +60 -468
- package/android/src/main/cpp/CMakeLists.txt +54 -0
- package/android/src/main/cpp/pdfcanvas-jni.cpp +262 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasNative.java +167 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfErrorCode.java +14 -4
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfRasterException.java +10 -4
- package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +160 -376
- package/android/tools/compile-gate.sh +81 -101
- package/dist/controller.d.ts +14 -0
- package/dist/controller.js +4 -3
- package/dist/index.native.d.ts +5 -0
- package/dist/index.native.js +5 -0
- package/dist/index.web.d.ts +10 -0
- package/dist/index.web.js +10 -0
- package/dist/rasterizer/index.native.d.ts +8 -12
- package/dist/rasterizer/index.native.js +15 -21
- package/dist/rasterizer/native-bridge.d.ts +51 -75
- package/dist/rasterizer/native-bridge.js +49 -33
- package/dist/rasterizer/native.d.ts +45 -0
- package/dist/rasterizer/native.js +123 -0
- package/dist/react/usePdfDocument.d.ts +5 -5
- package/dist/react/usePdfLayer.d.ts +10 -1
- package/dist/react/usePdfLayer.js +10 -2
- package/dist/skia/index.native.d.ts +7 -3
- package/dist/skia/index.native.js +7 -3
- package/dist/skia/index.web.d.ts +4 -2
- package/dist/skia/index.web.js +4 -2
- package/dist/testing/scenes.d.ts +3 -3
- package/dist/testing/scenes.js +4 -4
- package/dist/types.d.ts +36 -42
- package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +234 -536
- package/native/.clangd +5 -0
- package/native/CMakeLists.txt +89 -0
- package/native/core/include/pdfcanvas/document.h +90 -0
- package/native/core/include/pdfcanvas/error.h +47 -0
- package/native/core/include/pdfcanvas/jsi.h +18 -0
- package/native/core/include/pdfcanvas/jsi_config.h +30 -0
- package/native/core/include/pdfcanvas/library.h +28 -0
- package/native/core/include/pdfcanvas/pixels.h +92 -0
- package/native/core/include/pdfcanvas/service.h +87 -0
- package/native/core/include/pdfcanvas/slots.h +113 -0
- package/native/core/include/pdfcanvas/types.h +110 -0
- package/native/core/pdfcanvas-core.cmake +22 -0
- package/native/core/src/document.cpp +516 -0
- package/native/core/src/error.cpp +29 -0
- package/native/core/src/jsi.cpp +66 -0
- package/native/core/src/library.cpp +52 -0
- package/native/core/src/pixels.cpp +45 -0
- package/native/core/src/service.cpp +156 -0
- package/native/core/src/slots.cpp +139 -0
- package/native/tests/fixtures.cpp +451 -0
- package/native/tests/fixtures.h +96 -0
- package/native/tests/harness.h +122 -0
- package/native/tests/main.cpp +64 -0
- package/native/tests/pixels_util.h +126 -0
- package/native/tests/test_document.cpp +485 -0
- package/native/tests/test_service.cpp +130 -0
- package/native/tests/test_slots.cpp +250 -0
- package/package.json +38 -8
- package/react-native.config.js +5 -2
- package/scripts/fetch-pdfium.mjs +487 -0
- package/scripts/pdfium-manifest.json +46 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasCancellationTest.java +0 -226
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasConcurrencyTest.java +0 -239
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasDeviceTimingTest.java +0 -251
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasFailureTest.java +0 -221
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasJsiTransportTest.java +0 -783
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPhaseTimingTest.java +0 -1388
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPoisonGuardTest.java +0 -98
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasQuirksTest.java +0 -324
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasRasterizerTest.java +0 -775
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PoisonsTheProcess.java +0 -20
- package/android/src/jsi/cpp/CMakeLists.txt +0 -33
- package/android/src/jsi/cpp/pdfcanvas-jsi.cpp +0 -469
- package/android/src/jsi/java/tools/reekon/pdfcanvas/jsi/PdfCanvasPixelBridge.java +0 -140
- package/android/src/main/java/tools/reekon/pdfcanvas/PageGeometry.java +0 -45
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasRasterizer.java +0 -564
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfPlatformQuirks.java +0 -277
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfPreflight.java +0 -214
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfRendererPool.java +0 -234
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterCancellation.java +0 -47
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterPixels.java +0 -143
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterRequest.java +0 -95
- package/dist/rasterizer/android.d.ts +0 -121
- package/dist/rasterizer/android.js +0 -126
- package/dist/rasterizer/ios.d.ts +0 -84
- package/dist/rasterizer/ios.js +0 -155
- package/ios/Package.swift +0 -58
- package/ios/Sources/PdfCanvasCore/PdfCanvasCancellation.m +0 -38
- package/ios/Sources/PdfCanvasCore/PdfCanvasDocumentPool.m +0 -389
- package/ios/Sources/PdfCanvasCore/PdfCanvasErrorCode.m +0 -57
- package/ios/Sources/PdfCanvasCore/PdfCanvasPageGeometry.m +0 -44
- package/ios/Sources/PdfCanvasCore/PdfCanvasPageTransform.m +0 -82
- package/ios/Sources/PdfCanvasCore/PdfCanvasPixelSink.m +0 -37
- package/ios/Sources/PdfCanvasCore/PdfCanvasPreflight.m +0 -158
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterPixels.m +0 -80
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterRequest.m +0 -65
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterizer.m +0 -548
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasCancellation.h +0 -48
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasCore.h +0 -26
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasDocumentPool.h +0 -122
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasErrorCode.h +0 -80
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageGeometry.h +0 -79
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageTransform.h +0 -129
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPixelSink.h +0 -100
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPreflight.h +0 -65
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterPixels.h +0 -111
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterRequest.h +0 -85
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterizer.h +0 -163
- package/ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm +0 -297
- package/ios/Sources/PdfCanvasSlots/include/PdfCanvasSlots.h +0 -140
- package/ios/Tests/PdfCanvasCoreTests/ConcurrencyTests.swift +0 -358
- package/ios/Tests/PdfCanvasCoreTests/FailureTests.swift +0 -262
- package/ios/Tests/PdfCanvasCoreTests/GeometryTests.swift +0 -173
- package/ios/Tests/PdfCanvasCoreTests/PreviewTests.swift +0 -114
- package/ios/Tests/PdfCanvasCoreTests/RasterAssertions.swift +0 -158
- package/ios/Tests/PdfCanvasCoreTests/RasterizerTests.swift +0 -544
- package/ios/Tests/PdfCanvasCoreTests/SinkTests.swift +0 -287
- package/ios/Tests/PdfCanvasCoreTests/SlotsTests.swift +0 -396
- package/ios/Tests/PdfCanvasCoreTests/TestPdfs.swift +0 -639
- package/ios/Tests/PdfCanvasCoreTests/TimingTests.swift +0 -179
- package/ios/Tests/PdfCanvasCoreTests/TransformTests.swift +0 -283
|
@@ -1,1388 +0,0 @@
|
|
|
1
|
-
package tools.reekon.pdfcanvas;
|
|
2
|
-
|
|
3
|
-
import static org.junit.Assert.assertEquals;
|
|
4
|
-
import static org.junit.Assert.assertTrue;
|
|
5
|
-
|
|
6
|
-
import android.content.Context;
|
|
7
|
-
import android.graphics.Bitmap;
|
|
8
|
-
import android.graphics.Color;
|
|
9
|
-
import android.graphics.Matrix;
|
|
10
|
-
import android.graphics.pdf.PdfRenderer;
|
|
11
|
-
import android.os.ParcelFileDescriptor;
|
|
12
|
-
import android.util.Base64;
|
|
13
|
-
import android.util.Log;
|
|
14
|
-
|
|
15
|
-
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
16
|
-
import androidx.test.platform.app.InstrumentationRegistry;
|
|
17
|
-
|
|
18
|
-
import org.junit.Test;
|
|
19
|
-
import org.junit.runner.RunWith;
|
|
20
|
-
|
|
21
|
-
import java.io.File;
|
|
22
|
-
import java.io.IOException;
|
|
23
|
-
import java.nio.ByteBuffer;
|
|
24
|
-
import java.util.ArrayList;
|
|
25
|
-
import java.util.Arrays;
|
|
26
|
-
import java.util.List;
|
|
27
|
-
import java.util.concurrent.ConcurrentLinkedQueue;
|
|
28
|
-
import java.util.concurrent.CountDownLatch;
|
|
29
|
-
import java.util.concurrent.ExecutorService;
|
|
30
|
-
import java.util.concurrent.Executors;
|
|
31
|
-
import java.util.concurrent.TimeUnit;
|
|
32
|
-
import java.util.concurrent.atomic.AtomicInteger;
|
|
33
|
-
import java.util.concurrent.atomic.AtomicLong;
|
|
34
|
-
import java.util.concurrent.atomic.AtomicReference;
|
|
35
|
-
|
|
36
|
-
import tools.reekon.pdfcanvas.jsi.PdfCanvasPixelBridge;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* MEASUREMENT, NOT VERIFICATION — the same contract as
|
|
40
|
-
* {@link PdfCanvasDeviceTimingTest}, one level finer. That file answers "how long
|
|
41
|
-
* does a tile take". This one answers the two questions a tile timing cannot:
|
|
42
|
-
*
|
|
43
|
-
* <ol>
|
|
44
|
-
* <li>WHERE inside a tile the time goes — {@code openPage}, bitmap allocation,
|
|
45
|
-
* the white fill, {@code PdfRenderer.render}, the copy to a Java
|
|
46
|
-
* {@code byte[]}, and the base64 encode, each timed separately.
|
|
47
|
-
* <li>WHETHER the renderer pool actually buys wall-clock. The controller
|
|
48
|
-
* renders an epoch's pieces strictly one at a time; the whole case for
|
|
49
|
-
* parallelising it rests on N renderers finishing N tiles in meaningfully
|
|
50
|
-
* less than N times one tile. That is measured here at N = 2, 3, 4 and 6.
|
|
51
|
-
* </ol>
|
|
52
|
-
*
|
|
53
|
-
* <h3>What is NOT measured here, and cannot be</h3>
|
|
54
|
-
*
|
|
55
|
-
* The bridge hop and everything past it — the base64 STRING crossing into JS,
|
|
56
|
-
* the hand-rolled {@code decodeBase64} in {@code src/rasterizer/android.ts}, and
|
|
57
|
-
* {@code Skia.Data.fromBytes} + {@code MakeImage}. None of that exists in a
|
|
58
|
-
* process with no React Native in it, which this test build deliberately is (see
|
|
59
|
-
* {@code android/build.gradle}). What this file CAN do about them is state the
|
|
60
|
-
* exact byte volumes they are handed, which it logs: raw bytes, base64 chars,
|
|
61
|
-
* and the encode cost that produced them.
|
|
62
|
-
*
|
|
63
|
-
* <h3>Why the phase harness re-implements the render instead of calling it</h3>
|
|
64
|
-
*
|
|
65
|
-
* {@link PdfCanvasRasterizer#render} is one opaque call; splitting it from
|
|
66
|
-
* outside is impossible and adding timing hooks to it would put measurement
|
|
67
|
-
* scaffolding in production code forever. So {@link #renderPhases} mirrors
|
|
68
|
-
* {@code renderLeased} step for step — and then {@link #phaseHarnessMatchesTheRealRender}
|
|
69
|
-
* proves the mirror is exact by comparing its pixels byte-for-byte against the
|
|
70
|
-
* real thing. A drifted copy would measure a render nobody performs.
|
|
71
|
-
*/
|
|
72
|
-
@RunWith(AndroidJUnit4.class)
|
|
73
|
-
public class PdfCanvasPhaseTimingTest {
|
|
74
|
-
|
|
75
|
-
private static final String TAG = "PDFCANVAS";
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* The viewport the field report was taken at: 131.25 device px per PDF point,
|
|
79
|
-
* i.e. zoom 50x on a 2.625 dpr phone. Everything in this file is measured
|
|
80
|
-
* there, because the complaint is about that state and cost does not
|
|
81
|
-
* necessarily scale linearly out of it.
|
|
82
|
-
*/
|
|
83
|
-
private static final float PROBE_SCALE = 131.25f;
|
|
84
|
-
|
|
85
|
-
/** {@code maxPieceDimension}'s current default. */
|
|
86
|
-
private static final int TILE_PX = 1024;
|
|
87
|
-
|
|
88
|
-
/** The probe document's shape: 6 x US Letter, stacked. */
|
|
89
|
-
private static final int PROBE_PAGES = 6;
|
|
90
|
-
|
|
91
|
-
private static final int SAMPLES = 20;
|
|
92
|
-
|
|
93
|
-
/** Same convention as {@link PdfCanvasDeviceTimingTest}: push one to use it. */
|
|
94
|
-
private static final String REAL_PDF = "customer.pdf";
|
|
95
|
-
|
|
96
|
-
private static Context ctx() {
|
|
97
|
-
return InstrumentationRegistry.getInstrumentation().getTargetContext();
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/* ================================================================ *
|
|
101
|
-
* Fixtures
|
|
102
|
-
* ================================================================ */
|
|
103
|
-
|
|
104
|
-
/** The real drawing if one was pushed, else the synthetic 6-page letter sheet. */
|
|
105
|
-
private static File fixture() throws IOException {
|
|
106
|
-
File dir = ctx().getExternalFilesDir(null);
|
|
107
|
-
File real = dir == null ? null : new File(dir, REAL_PDF);
|
|
108
|
-
if (real != null && real.isFile()) {
|
|
109
|
-
return real;
|
|
110
|
-
}
|
|
111
|
-
Log.w(
|
|
112
|
-
TAG,
|
|
113
|
-
"no "
|
|
114
|
-
+ REAL_PDF
|
|
115
|
-
+ " — falling back to the SYNTHETIC 6-page letter pattern, which is "
|
|
116
|
-
+ "vector rects and lines and is therefore an OPTIMISTIC bound. Push a "
|
|
117
|
-
+ "real drawing to:\n adb push drawing.pdf /sdcard/Android/data/"
|
|
118
|
-
+ ctx().getPackageName()
|
|
119
|
-
+ "/files/"
|
|
120
|
-
+ REAL_PDF);
|
|
121
|
-
return TestPdfs.pattern(
|
|
122
|
-
ctx(), "phase-letter6.pdf", TestPdfs.LETTER_W, TestPdfs.LETTER_H, PROBE_PAGES);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
private static boolean usingRealPdf() {
|
|
126
|
-
File dir = ctx().getExternalFilesDir(null);
|
|
127
|
-
return dir != null && new File(dir, REAL_PDF).isFile();
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* The six pieces of the field report's detail epoch, laid out the way the
|
|
132
|
-
* planner lays them out: a 2-wide, 3-tall grid of {@code TILE_PX} tiles anchored
|
|
133
|
-
* at the visible rect's origin.
|
|
134
|
-
*
|
|
135
|
-
* The real epoch's right and bottom pieces are CLIPPED (the detail rect is
|
|
136
|
-
* 1345 x 2133 px, so the third row is 85 px tall). Full 1024 squares are used
|
|
137
|
-
* throughout anyway: the question here is what one full-size piece costs and
|
|
138
|
-
* how N of them scale, and a clipped piece only ever costs less.
|
|
139
|
-
*/
|
|
140
|
-
private static List<RasterRequest> epochPieces(int count, int page) {
|
|
141
|
-
float side = TILE_PX / PROBE_SCALE; // 7.8019 pt
|
|
142
|
-
List<RasterRequest> out = new ArrayList<>(count);
|
|
143
|
-
for (int i = 0; i < count; i++) {
|
|
144
|
-
float x = 242.9f + (i % 2) * side;
|
|
145
|
-
float y = 549.4f + (i / 2) * side;
|
|
146
|
-
out.add(
|
|
147
|
-
new RasterRequest(
|
|
148
|
-
page, x, y, side, side, PROBE_SCALE, false, RasterRequest.BACKGROUND_WHITE));
|
|
149
|
-
}
|
|
150
|
-
return out;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/* ================================================================ *
|
|
154
|
-
* 1. Phase decomposition
|
|
155
|
-
* ================================================================ */
|
|
156
|
-
|
|
157
|
-
/** Every timed segment of one tile, in nanoseconds. */
|
|
158
|
-
private static final class Phases {
|
|
159
|
-
long openPage;
|
|
160
|
-
long alloc;
|
|
161
|
-
long erase;
|
|
162
|
-
long render;
|
|
163
|
-
long extract;
|
|
164
|
-
long base64;
|
|
165
|
-
long teardown;
|
|
166
|
-
long total;
|
|
167
|
-
int rawBytes;
|
|
168
|
-
int base64Chars;
|
|
169
|
-
int rowBytes;
|
|
170
|
-
byte[] bytes;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* MIRRORS {@code PdfCanvasRasterizer.renderLeased} exactly, plus the base64
|
|
175
|
-
* encode that {@code PdfCanvasModule.render} does on the same worker thread.
|
|
176
|
-
* Pinned against the real render by {@link #phaseHarnessMatchesTheRealRender}.
|
|
177
|
-
*/
|
|
178
|
-
private static Phases renderPhases(PdfRenderer renderer, RasterRequest request) {
|
|
179
|
-
Phases p = new Phases();
|
|
180
|
-
int pxW = request.pixelWidth();
|
|
181
|
-
int pxH = request.pixelHeight();
|
|
182
|
-
|
|
183
|
-
long t0 = System.nanoTime();
|
|
184
|
-
PdfRenderer.Page page = renderer.openPage(request.page);
|
|
185
|
-
long t1 = System.nanoTime();
|
|
186
|
-
|
|
187
|
-
Bitmap bitmap = Bitmap.createBitmap(pxW, pxH, Bitmap.Config.ARGB_8888);
|
|
188
|
-
long t2 = System.nanoTime();
|
|
189
|
-
|
|
190
|
-
bitmap.eraseColor(
|
|
191
|
-
request.background == RasterRequest.BACKGROUND_TRANSPARENT
|
|
192
|
-
? Color.TRANSPARENT
|
|
193
|
-
: Color.WHITE);
|
|
194
|
-
long t3 = System.nanoTime();
|
|
195
|
-
|
|
196
|
-
Matrix matrix = new Matrix();
|
|
197
|
-
matrix.setScale(request.scale, request.scale);
|
|
198
|
-
matrix.postTranslate(-request.docX * request.scale, -request.docY * request.scale);
|
|
199
|
-
page.render(bitmap, null, matrix, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);
|
|
200
|
-
long t4 = System.nanoTime();
|
|
201
|
-
|
|
202
|
-
int rowBytes = bitmap.getRowBytes();
|
|
203
|
-
byte[] bytes = new byte[bitmap.getByteCount()];
|
|
204
|
-
bitmap.copyPixelsToBuffer(ByteBuffer.wrap(bytes));
|
|
205
|
-
long t5 = System.nanoTime();
|
|
206
|
-
|
|
207
|
-
String encoded = Base64.encodeToString(bytes, Base64.NO_WRAP);
|
|
208
|
-
long t6 = System.nanoTime();
|
|
209
|
-
|
|
210
|
-
bitmap.recycle();
|
|
211
|
-
page.close();
|
|
212
|
-
long t7 = System.nanoTime();
|
|
213
|
-
|
|
214
|
-
p.openPage = t1 - t0;
|
|
215
|
-
p.alloc = t2 - t1;
|
|
216
|
-
p.erase = t3 - t2;
|
|
217
|
-
p.render = t4 - t3;
|
|
218
|
-
p.extract = t5 - t4;
|
|
219
|
-
p.base64 = t6 - t5;
|
|
220
|
-
p.teardown = t7 - t6;
|
|
221
|
-
p.total = t7 - t0;
|
|
222
|
-
p.rawBytes = bytes.length;
|
|
223
|
-
p.base64Chars = encoded.length();
|
|
224
|
-
p.rowBytes = rowBytes;
|
|
225
|
-
p.bytes = bytes;
|
|
226
|
-
return p;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* CATCHES: {@link #renderPhases} drifting away from
|
|
231
|
-
* {@code PdfCanvasRasterizer.renderLeased}, which would make every number in
|
|
232
|
-
* this file a measurement of code that never runs in production.
|
|
233
|
-
*
|
|
234
|
-
* Byte-for-byte, not "close enough" — the two paths build the same matrix from
|
|
235
|
-
* the same request, so any difference at all means a step was dropped or
|
|
236
|
-
* reordered.
|
|
237
|
-
*/
|
|
238
|
-
@Test
|
|
239
|
-
public void phaseHarnessMatchesTheRealRender() throws Exception {
|
|
240
|
-
File pdf = fixture();
|
|
241
|
-
RasterRequest request = epochPieces(1, 0).get(0);
|
|
242
|
-
|
|
243
|
-
PdfCanvasRasterizer real = PdfCanvasRasterizer.openFile(pdf, null, 1);
|
|
244
|
-
RasterPixels expected;
|
|
245
|
-
try {
|
|
246
|
-
expected = real.render(request, null);
|
|
247
|
-
} finally {
|
|
248
|
-
real.close();
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
ParcelFileDescriptor pfd =
|
|
252
|
-
ParcelFileDescriptor.open(pdf, ParcelFileDescriptor.MODE_READ_ONLY);
|
|
253
|
-
PdfRenderer renderer = new PdfRenderer(pfd);
|
|
254
|
-
Phases got;
|
|
255
|
-
try {
|
|
256
|
-
got = renderPhases(renderer, request);
|
|
257
|
-
} finally {
|
|
258
|
-
renderer.close();
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
assertEquals("rowBytes", expected.rowBytes, got.rowBytes);
|
|
262
|
-
assertEquals("byte count", expected.bytes.length, got.rawBytes);
|
|
263
|
-
RasterPixels mirrored =
|
|
264
|
-
new RasterPixels(
|
|
265
|
-
got.bytes,
|
|
266
|
-
expected.width,
|
|
267
|
-
expected.height,
|
|
268
|
-
got.rowBytes,
|
|
269
|
-
RasterPixels.FORMAT_RGBA_8888);
|
|
270
|
-
PdfCanvasRasterizerTest.Diff diff =
|
|
271
|
-
PdfCanvasRasterizerTest.compareRegion(mirrored, expected, 0, 0);
|
|
272
|
-
assertEquals(
|
|
273
|
-
"the phase harness must render exactly what the rasterizer renders: " + diff,
|
|
274
|
-
0,
|
|
275
|
-
diff.mismatches);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* The decomposition. One 1024 px tile at the field report's scale, broken into
|
|
280
|
-
* its six phases plus teardown, {@value #SAMPLES} times.
|
|
281
|
-
*
|
|
282
|
-
* The tile WALKS between samples, for the reason
|
|
283
|
-
* {@link PdfCanvasDeviceTimingTest#report} gives: re-rendering one rect
|
|
284
|
-
* measures PDFium's cache, and panning does not get that.
|
|
285
|
-
*/
|
|
286
|
-
@Test
|
|
287
|
-
public void phaseBreakdownOfOneTileAtProbeScale() throws Exception {
|
|
288
|
-
File pdf = fixture();
|
|
289
|
-
ParcelFileDescriptor pfd =
|
|
290
|
-
ParcelFileDescriptor.open(pdf, ParcelFileDescriptor.MODE_READ_ONLY);
|
|
291
|
-
PdfRenderer renderer = new PdfRenderer(pfd);
|
|
292
|
-
try {
|
|
293
|
-
Log.i(
|
|
294
|
-
TAG,
|
|
295
|
-
"PHASES fixture="
|
|
296
|
-
+ (usingRealPdf() ? "REAL " + REAL_PDF : "SYNTHETIC 6-page letter")
|
|
297
|
-
+ " "
|
|
298
|
-
+ pdf.length()
|
|
299
|
-
+ " bytes, "
|
|
300
|
-
+ renderer.getPageCount()
|
|
301
|
-
+ " pages, scale="
|
|
302
|
-
+ PROBE_SCALE
|
|
303
|
-
+ " devpx/pt");
|
|
304
|
-
for (int px : new int[] {TILE_PX, 512}) {
|
|
305
|
-
phaseReport(renderer, px, Math.min(PROBE_PAGES, renderer.getPageCount()));
|
|
306
|
-
}
|
|
307
|
-
} finally {
|
|
308
|
-
renderer.close();
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
private static void phaseReport(PdfRenderer renderer, int px, int pages) {
|
|
313
|
-
float side = px / PROBE_SCALE;
|
|
314
|
-
long[] openPage = new long[SAMPLES];
|
|
315
|
-
long[] alloc = new long[SAMPLES];
|
|
316
|
-
long[] erase = new long[SAMPLES];
|
|
317
|
-
long[] render = new long[SAMPLES];
|
|
318
|
-
long[] extract = new long[SAMPLES];
|
|
319
|
-
long[] base64 = new long[SAMPLES];
|
|
320
|
-
long[] teardown = new long[SAMPLES];
|
|
321
|
-
long[] total = new long[SAMPLES];
|
|
322
|
-
Phases last = null;
|
|
323
|
-
|
|
324
|
-
for (int i = 0; i < SAMPLES; i++) {
|
|
325
|
-
// Walk over the page BODY so no two samples share PDFium's per-region
|
|
326
|
-
// cache — and, just as importantly, so no sample lands in the margin. A
|
|
327
|
-
// 7.8 pt tile stepped by 3 x itself never leaves the top-left corner of a
|
|
328
|
-
// letter page, which on a real drawing is white space: the first run of
|
|
329
|
-
// this test measured PdfRenderer.render at 0.076 ms for exactly that
|
|
330
|
-
// reason. These 20 positions span 60..500 pt across and 90..540 pt down.
|
|
331
|
-
float x = 60f + (i % 5) * 110f;
|
|
332
|
-
float y = 90f + (i / 5) * 150f;
|
|
333
|
-
RasterRequest request =
|
|
334
|
-
new RasterRequest(
|
|
335
|
-
i % Math.max(1, pages),
|
|
336
|
-
x,
|
|
337
|
-
y,
|
|
338
|
-
side,
|
|
339
|
-
side,
|
|
340
|
-
PROBE_SCALE,
|
|
341
|
-
false,
|
|
342
|
-
RasterRequest.BACKGROUND_WHITE);
|
|
343
|
-
Phases p = renderPhases(renderer, request);
|
|
344
|
-
openPage[i] = p.openPage;
|
|
345
|
-
alloc[i] = p.alloc;
|
|
346
|
-
erase[i] = p.erase;
|
|
347
|
-
render[i] = p.render;
|
|
348
|
-
extract[i] = p.extract;
|
|
349
|
-
base64[i] = p.base64;
|
|
350
|
-
teardown[i] = p.teardown;
|
|
351
|
-
total[i] = p.total;
|
|
352
|
-
p.bytes = null; // 4 MiB per sample; do not hold 20 of them.
|
|
353
|
-
last = p;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
Log.i(
|
|
357
|
-
TAG,
|
|
358
|
-
"PHASES "
|
|
359
|
-
+ px
|
|
360
|
-
+ "px@"
|
|
361
|
-
+ PROBE_SCALE
|
|
362
|
-
+ "x n="
|
|
363
|
-
+ SAMPLES
|
|
364
|
-
+ " raw="
|
|
365
|
-
+ (last == null ? 0 : last.rawBytes)
|
|
366
|
-
+ "B base64="
|
|
367
|
-
+ (last == null ? 0 : last.base64Chars)
|
|
368
|
-
+ "chars rowBytes="
|
|
369
|
-
+ (last == null ? 0 : last.rowBytes));
|
|
370
|
-
line("1 PdfRenderer.render", render, total);
|
|
371
|
-
line("2 Bitmap alloc+zero", alloc, total);
|
|
372
|
-
line("2b eraseColor(WHITE)", erase, total);
|
|
373
|
-
line("3 extract to byte[]", extract, total);
|
|
374
|
-
line("4 base64 encode ", base64, total);
|
|
375
|
-
line("- openPage ", openPage, total);
|
|
376
|
-
line("- recycle+close ", teardown, total);
|
|
377
|
-
line("= TOTAL ", total, total);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/** One phase row: p50/p95 in microseconds, plus its share of the tile's p50. */
|
|
381
|
-
private static void line(String label, long[] ns, long[] totalNs) {
|
|
382
|
-
long[] s = ns.clone();
|
|
383
|
-
Arrays.sort(s);
|
|
384
|
-
long[] t = totalNs.clone();
|
|
385
|
-
Arrays.sort(t);
|
|
386
|
-
long p50 = percentile(s, 50);
|
|
387
|
-
long totalP50 = Math.max(1L, percentile(t, 50));
|
|
388
|
-
Log.i(
|
|
389
|
-
TAG,
|
|
390
|
-
String.format(
|
|
391
|
-
" %s p50=%8.3f ms p95=%8.3f ms min=%8.3f max=%8.3f share(p50)=%5.1f%%",
|
|
392
|
-
label,
|
|
393
|
-
p50 / 1e6,
|
|
394
|
-
percentile(s, 95) / 1e6,
|
|
395
|
-
s[0] / 1e6,
|
|
396
|
-
s[s.length - 1] / 1e6,
|
|
397
|
-
(100.0 * p50) / totalP50));
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
/* ================================================================ *
|
|
401
|
-
* 2. Concurrency headroom — the number the fix depends on
|
|
402
|
-
* ================================================================ */
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* N tiles one at a time versus N tiles across N threads through a pool of N,
|
|
406
|
-
* for N = 2, 3, 4 and 6.
|
|
407
|
-
*
|
|
408
|
-
* This is the whole case for parallelising the controller's per-piece loop, and
|
|
409
|
-
* it is reported rather than asserted: a speedup assertion fails on a warm
|
|
410
|
-
* phone and teaches nobody anything. What the numbers can show is the failure
|
|
411
|
-
* mode the pool's own header warns about — AOSP has historically serialised
|
|
412
|
-
* every {@code PdfRenderer} native call behind a process-wide PDFium lock, and
|
|
413
|
-
* if that lock is present on this build the parallel row will match the serial
|
|
414
|
-
* row and no amount of controller work will change it.
|
|
415
|
-
*
|
|
416
|
-
* N = 6 exceeds {@link PdfRendererPool#MAX_POOL_SIZE}, deliberately: the field
|
|
417
|
-
* report's epoch is 6 pieces, so the honest question is what 6 pieces cost with
|
|
418
|
-
* the pool as it is actually capped, not with a hypothetical pool of 6.
|
|
419
|
-
*/
|
|
420
|
-
@Test
|
|
421
|
-
public void poolConcurrencyScalingAtProbeScale() throws Exception {
|
|
422
|
-
File pdf = fixture();
|
|
423
|
-
Log.i(
|
|
424
|
-
TAG,
|
|
425
|
-
"CONCURRENCY fixture="
|
|
426
|
-
+ (usingRealPdf() ? "REAL " + REAL_PDF : "SYNTHETIC 6-page letter")
|
|
427
|
-
+ " tile="
|
|
428
|
-
+ TILE_PX
|
|
429
|
-
+ "px scale="
|
|
430
|
-
+ PROBE_SCALE
|
|
431
|
-
+ " MAX_POOL_SIZE="
|
|
432
|
-
+ PdfRendererPool.MAX_POOL_SIZE
|
|
433
|
-
+ " cores="
|
|
434
|
-
+ Runtime.getRuntime().availableProcessors());
|
|
435
|
-
|
|
436
|
-
for (int n : new int[] {2, 3, 4, 6}) {
|
|
437
|
-
List<RasterRequest> pieces = epochPieces(n, 0);
|
|
438
|
-
long serial = bestOfFive(() -> renderSerially(pdf, pieces));
|
|
439
|
-
int poolSize = Math.min(n, PdfRendererPool.MAX_POOL_SIZE);
|
|
440
|
-
long parallel = bestOfFive(() -> renderInParallel(pdf, pieces, n, poolSize));
|
|
441
|
-
Log.i(
|
|
442
|
-
TAG,
|
|
443
|
-
String.format(
|
|
444
|
-
"CONCURRENCY n=%d serial=%5d ms (%6.1f ms/tile) parallel(pool=%d,threads=%d)"
|
|
445
|
-
+ "=%5d ms (%6.1f ms/tile) speedup=%.2fx",
|
|
446
|
-
n,
|
|
447
|
-
serial,
|
|
448
|
-
serial / (double) n,
|
|
449
|
-
poolSize,
|
|
450
|
-
n,
|
|
451
|
-
parallel,
|
|
452
|
-
parallel / (double) n,
|
|
453
|
-
serial / (double) Math.max(1L, parallel)));
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
private interface Run {
|
|
458
|
-
long runMs() throws Exception;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
/**
|
|
462
|
-
* BEST of five, not median.
|
|
463
|
-
*
|
|
464
|
-
* The opposite of what {@link PdfCanvasDeviceTimingTest} wants for a cadence
|
|
465
|
-
* constant, and for the opposite reason: this test is a RATIO of two runs, and
|
|
466
|
-
* a thermal or scheduler stall lands in whichever run it happens to hit and
|
|
467
|
-
* moves the ratio in a direction that means nothing. The floor of each side is
|
|
468
|
-
* the number least polluted by the phone doing something else — and the user is
|
|
469
|
-
* actively using this one, so that is not a hypothetical.
|
|
470
|
-
*/
|
|
471
|
-
private static long bestOfFive(Run run) throws Exception {
|
|
472
|
-
long best = Long.MAX_VALUE;
|
|
473
|
-
for (int i = 0; i < 5; i++) {
|
|
474
|
-
best = Math.min(best, run.runMs());
|
|
475
|
-
}
|
|
476
|
-
return best;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
/**
|
|
480
|
-
* The Java-side work of ONE tile as {@code PdfCanvasModule} actually performs
|
|
481
|
-
* it: the raster, and then the base64 encode of its pixels on the same worker
|
|
482
|
-
* thread, before the promise resolves.
|
|
483
|
-
*
|
|
484
|
-
* Including the encode is not padding. It is 75-80% of the Java cost of a
|
|
485
|
-
* 1024 px tile (see {@link #phaseBreakdownOfOneTileAtProbeScale}), it sits in
|
|
486
|
-
* the serial chain the user waits on, and it is exactly as parallelisable as
|
|
487
|
-
* the render — so a concurrency measurement that left it out would answer a
|
|
488
|
-
* question nobody asked.
|
|
489
|
-
*/
|
|
490
|
-
private static void renderAndEncode(PdfCanvasRasterizer r, RasterRequest request)
|
|
491
|
-
throws PdfRasterException {
|
|
492
|
-
RasterPixels pixels = r.render(request, null);
|
|
493
|
-
// The result is used, so neither the encode nor the raster can be optimised
|
|
494
|
-
// away — and the length is the payload the bridge is handed.
|
|
495
|
-
if (Base64.encodeToString(pixels.bytes, Base64.NO_WRAP).isEmpty()) {
|
|
496
|
-
throw new AssertionError("empty base64 for " + request);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
private static long renderSerially(File pdf, List<RasterRequest> pieces) throws Exception {
|
|
501
|
-
PdfCanvasRasterizer r = PdfCanvasRasterizer.openFile(pdf, null, 1);
|
|
502
|
-
try {
|
|
503
|
-
warm(r, pieces, 1);
|
|
504
|
-
long t0 = System.nanoTime();
|
|
505
|
-
for (RasterRequest request : pieces) {
|
|
506
|
-
renderAndEncode(r, request);
|
|
507
|
-
}
|
|
508
|
-
return (System.nanoTime() - t0) / 1_000_000L;
|
|
509
|
-
} finally {
|
|
510
|
-
r.close();
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
private static long renderInParallel(
|
|
515
|
-
File pdf, List<RasterRequest> pieces, int threads, int poolSize) throws Exception {
|
|
516
|
-
PdfCanvasRasterizer r = PdfCanvasRasterizer.openFile(pdf, null, poolSize);
|
|
517
|
-
assertEquals("pool size", poolSize, r.poolSize());
|
|
518
|
-
ExecutorService exec = Executors.newFixedThreadPool(threads);
|
|
519
|
-
CountDownLatch ready = new CountDownLatch(threads);
|
|
520
|
-
CountDownLatch go = new CountDownLatch(1);
|
|
521
|
-
CountDownLatch done = new CountDownLatch(threads);
|
|
522
|
-
AtomicReference<Throwable> failure = new AtomicReference<>();
|
|
523
|
-
long t0;
|
|
524
|
-
try {
|
|
525
|
-
// Warms EVERY renderer in the pool before the clock starts. Without this
|
|
526
|
-
// the parallel run pays for N independent page parses that the serial run
|
|
527
|
-
// paid once, and the comparison measures the warm-up rather than the lock.
|
|
528
|
-
warm(r, pieces, poolSize);
|
|
529
|
-
|
|
530
|
-
for (int t = 0; t < threads; t++) {
|
|
531
|
-
final RasterRequest request = pieces.get(t % pieces.size());
|
|
532
|
-
exec.execute(
|
|
533
|
-
() -> {
|
|
534
|
-
try {
|
|
535
|
-
ready.countDown();
|
|
536
|
-
go.await();
|
|
537
|
-
renderAndEncode(r, request);
|
|
538
|
-
} catch (Throwable e) {
|
|
539
|
-
failure.compareAndSet(null, e);
|
|
540
|
-
} finally {
|
|
541
|
-
done.countDown();
|
|
542
|
-
}
|
|
543
|
-
});
|
|
544
|
-
}
|
|
545
|
-
// Thread CREATION happens before the clock starts; only the wake-up from
|
|
546
|
-
// the latch is inside it. Without this barrier the first round of a
|
|
547
|
-
// 6-thread run charges the parallel side for spawning threads that the
|
|
548
|
-
// serial side never spawns.
|
|
549
|
-
assertTrue("threads did not start in 30s", ready.await(30, TimeUnit.SECONDS));
|
|
550
|
-
t0 = System.nanoTime();
|
|
551
|
-
go.countDown();
|
|
552
|
-
assertTrue("threads did not finish in 120s", done.await(120, TimeUnit.SECONDS));
|
|
553
|
-
} finally {
|
|
554
|
-
exec.shutdownNow();
|
|
555
|
-
r.close();
|
|
556
|
-
}
|
|
557
|
-
Throwable f = failure.get();
|
|
558
|
-
if (f != null) {
|
|
559
|
-
throw new AssertionError("a parallel render failed: " + f, f);
|
|
560
|
-
}
|
|
561
|
-
return (System.nanoTime() - t0) / 1_000_000L;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
/**
|
|
565
|
-
* Touches every renderer in the pool, by keeping {@code poolSize} renders in
|
|
566
|
-
* flight at once — a serial loop would warm renderer #0 and nothing else,
|
|
567
|
-
* because the pool hands back the one it just took.
|
|
568
|
-
*/
|
|
569
|
-
private static void warm(PdfCanvasRasterizer r, List<RasterRequest> pieces, int poolSize)
|
|
570
|
-
throws Exception {
|
|
571
|
-
if (poolSize <= 1) {
|
|
572
|
-
r.render(pieces.get(0), null);
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
575
|
-
ExecutorService exec = Executors.newFixedThreadPool(poolSize);
|
|
576
|
-
CountDownLatch done = new CountDownLatch(poolSize);
|
|
577
|
-
AtomicReference<Throwable> failure = new AtomicReference<>();
|
|
578
|
-
try {
|
|
579
|
-
for (int i = 0; i < poolSize; i++) {
|
|
580
|
-
final RasterRequest request = pieces.get(i % pieces.size());
|
|
581
|
-
exec.execute(
|
|
582
|
-
() -> {
|
|
583
|
-
try {
|
|
584
|
-
r.render(request, null);
|
|
585
|
-
} catch (Throwable e) {
|
|
586
|
-
failure.compareAndSet(null, e);
|
|
587
|
-
} finally {
|
|
588
|
-
done.countDown();
|
|
589
|
-
}
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
assertTrue("warm-up did not finish in 120s", done.await(120, TimeUnit.SECONDS));
|
|
593
|
-
} finally {
|
|
594
|
-
exec.shutdownNow();
|
|
595
|
-
}
|
|
596
|
-
Throwable f = failure.get();
|
|
597
|
-
if (f != null) {
|
|
598
|
-
throw new AssertionError("warm-up render failed: " + f, f);
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
/**
|
|
603
|
-
* WHY the parallel row of {@link #poolConcurrencyScalingAtProbeScale} comes out
|
|
604
|
-
* SLOWER, phase by phase.
|
|
605
|
-
*
|
|
606
|
-
* A wall-clock ratio says the pool does not scale; it cannot say whether that
|
|
607
|
-
* is a PDFium lock, the bitmap allocator, memory bandwidth, or nothing but
|
|
608
|
-
* thread wake-up skew. So this renders the same tiles twice — once serially on
|
|
609
|
-
* one renderer, once with one renderer per thread — through the SAME phase
|
|
610
|
-
* harness, and prints both breakdowns side by side. Whichever phase inflates is
|
|
611
|
-
* the mechanism.
|
|
612
|
-
*
|
|
613
|
-
* The base64 encode is included on both sides because it runs on the render
|
|
614
|
-
* worker in {@code PdfCanvasModule}, so it is part of what parallelising the
|
|
615
|
-
* controller's loop would actually spread out.
|
|
616
|
-
*
|
|
617
|
-
* {@code wake} is the gap between the starting gun and a thread's first
|
|
618
|
-
* instruction, recorded so that "the threads never overlapped" cannot be
|
|
619
|
-
* mistaken for "the work does not parallelise".
|
|
620
|
-
*/
|
|
621
|
-
@Test
|
|
622
|
-
public void phaseBreakdownUnderConcurrency() throws Exception {
|
|
623
|
-
File pdf = fixture();
|
|
624
|
-
int n = 4;
|
|
625
|
-
List<RasterRequest> pieces = epochPieces(n, 0);
|
|
626
|
-
|
|
627
|
-
List<ParcelFileDescriptor> fds = new ArrayList<>();
|
|
628
|
-
List<PdfRenderer> renderers = new ArrayList<>();
|
|
629
|
-
try {
|
|
630
|
-
for (int i = 0; i < n; i++) {
|
|
631
|
-
ParcelFileDescriptor pfd =
|
|
632
|
-
ParcelFileDescriptor.open(pdf, ParcelFileDescriptor.MODE_READ_ONLY);
|
|
633
|
-
fds.add(pfd);
|
|
634
|
-
renderers.add(new PdfRenderer(pfd));
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
// Warm every renderer on its own tile, on its own thread, so neither run
|
|
638
|
-
// pays for a first-touch page parse inside the timed section.
|
|
639
|
-
runAcross(renderers, pieces, n);
|
|
640
|
-
|
|
641
|
-
Phases[] serial = new Phases[n];
|
|
642
|
-
long serialStart = System.nanoTime();
|
|
643
|
-
for (int i = 0; i < n; i++) {
|
|
644
|
-
serial[i] = renderPhases(renderers.get(0), pieces.get(i));
|
|
645
|
-
serial[i].bytes = null;
|
|
646
|
-
}
|
|
647
|
-
long serialMs = (System.nanoTime() - serialStart) / 1_000_000L;
|
|
648
|
-
|
|
649
|
-
Concurrent concurrent = runAcross(renderers, pieces, n);
|
|
650
|
-
|
|
651
|
-
Log.i(
|
|
652
|
-
TAG,
|
|
653
|
-
String.format(
|
|
654
|
-
"CONC-PHASES n=%d tile=%dpx@%.2fx serial wall=%d ms parallel wall=%d ms"
|
|
655
|
-
+ " wake(max)=%.2f ms",
|
|
656
|
-
n, TILE_PX, PROBE_SCALE, serialMs, concurrent.wallMs, concurrent.maxWakeNs / 1e6));
|
|
657
|
-
comparePhase("1 PdfRenderer.render", serial, concurrent.phases, p -> p.render);
|
|
658
|
-
comparePhase("2 Bitmap alloc+zero ", serial, concurrent.phases, p -> p.alloc);
|
|
659
|
-
comparePhase("2b eraseColor(WHITE)", serial, concurrent.phases, p -> p.erase);
|
|
660
|
-
comparePhase("3 extract to byte[] ", serial, concurrent.phases, p -> p.extract);
|
|
661
|
-
comparePhase("4 base64 encode ", serial, concurrent.phases, p -> p.base64);
|
|
662
|
-
comparePhase("- openPage ", serial, concurrent.phases, p -> p.openPage);
|
|
663
|
-
comparePhase("- recycle+close ", serial, concurrent.phases, p -> p.teardown);
|
|
664
|
-
comparePhase("= TOTAL ", serial, concurrent.phases, p -> p.total);
|
|
665
|
-
} finally {
|
|
666
|
-
for (PdfRenderer renderer : renderers) {
|
|
667
|
-
renderer.close();
|
|
668
|
-
}
|
|
669
|
-
for (ParcelFileDescriptor pfd : fds) {
|
|
670
|
-
try {
|
|
671
|
-
pfd.close();
|
|
672
|
-
} catch (IOException ignored) {
|
|
673
|
-
// PdfRenderer.close() already closed it; nothing to salvage.
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
private static final class Concurrent {
|
|
680
|
-
Phases[] phases;
|
|
681
|
-
long wallMs;
|
|
682
|
-
long maxWakeNs;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
/** One tile per thread, one renderer per thread, released by a single latch. */
|
|
686
|
-
private static Concurrent runAcross(
|
|
687
|
-
List<PdfRenderer> renderers, List<RasterRequest> pieces, int n) throws Exception {
|
|
688
|
-
Phases[] out = new Phases[n];
|
|
689
|
-
long[] wake = new long[n];
|
|
690
|
-
ExecutorService exec = Executors.newFixedThreadPool(n);
|
|
691
|
-
CountDownLatch ready = new CountDownLatch(n);
|
|
692
|
-
CountDownLatch go = new CountDownLatch(1);
|
|
693
|
-
CountDownLatch done = new CountDownLatch(n);
|
|
694
|
-
AtomicReference<Throwable> failure = new AtomicReference<>();
|
|
695
|
-
long[] released = new long[1];
|
|
696
|
-
try {
|
|
697
|
-
for (int i = 0; i < n; i++) {
|
|
698
|
-
final int index = i;
|
|
699
|
-
exec.execute(
|
|
700
|
-
() -> {
|
|
701
|
-
try {
|
|
702
|
-
ready.countDown();
|
|
703
|
-
go.await();
|
|
704
|
-
wake[index] = System.nanoTime() - released[0];
|
|
705
|
-
Phases p = renderPhases(renderers.get(index), pieces.get(index));
|
|
706
|
-
p.bytes = null;
|
|
707
|
-
out[index] = p;
|
|
708
|
-
} catch (Throwable e) {
|
|
709
|
-
failure.compareAndSet(null, e);
|
|
710
|
-
} finally {
|
|
711
|
-
done.countDown();
|
|
712
|
-
}
|
|
713
|
-
});
|
|
714
|
-
}
|
|
715
|
-
// Every thread is parked on `go` before the clock starts, so thread
|
|
716
|
-
// CREATION is outside the measurement and only the wake-up is inside it.
|
|
717
|
-
assertTrue("threads did not start in 30s", ready.await(30, TimeUnit.SECONDS));
|
|
718
|
-
long t0 = System.nanoTime();
|
|
719
|
-
released[0] = t0;
|
|
720
|
-
go.countDown();
|
|
721
|
-
assertTrue("threads did not finish in 120s", done.await(120, TimeUnit.SECONDS));
|
|
722
|
-
Concurrent result = new Concurrent();
|
|
723
|
-
result.wallMs = (System.nanoTime() - t0) / 1_000_000L;
|
|
724
|
-
result.phases = out;
|
|
725
|
-
for (long w : wake) {
|
|
726
|
-
result.maxWakeNs = Math.max(result.maxWakeNs, w);
|
|
727
|
-
}
|
|
728
|
-
Throwable f = failure.get();
|
|
729
|
-
if (f != null) {
|
|
730
|
-
throw new AssertionError("a concurrent render failed: " + f, f);
|
|
731
|
-
}
|
|
732
|
-
return result;
|
|
733
|
-
} finally {
|
|
734
|
-
exec.shutdownNow();
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
private interface PhasePick {
|
|
739
|
-
long of(Phases p);
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
private static void comparePhase(
|
|
743
|
-
String label, Phases[] serial, Phases[] parallel, PhasePick pick) {
|
|
744
|
-
Log.i(
|
|
745
|
-
TAG,
|
|
746
|
-
String.format(
|
|
747
|
-
" %s serial p50=%8.3f ms max=%8.3f | parallel p50=%8.3f ms max=%8.3f"
|
|
748
|
-
+ " inflation=%.2fx",
|
|
749
|
-
label,
|
|
750
|
-
median(serial, pick) / 1e6,
|
|
751
|
-
max(serial, pick) / 1e6,
|
|
752
|
-
median(parallel, pick) / 1e6,
|
|
753
|
-
max(parallel, pick) / 1e6,
|
|
754
|
-
median(parallel, pick) / Math.max(1.0, median(serial, pick))));
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
private static double median(Phases[] samples, PhasePick pick) {
|
|
758
|
-
long[] values = new long[samples.length];
|
|
759
|
-
for (int i = 0; i < samples.length; i++) {
|
|
760
|
-
values[i] = samples[i] == null ? 0L : pick.of(samples[i]);
|
|
761
|
-
}
|
|
762
|
-
Arrays.sort(values);
|
|
763
|
-
return percentile(values, 50);
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
private static double max(Phases[] samples, PhasePick pick) {
|
|
767
|
-
long best = 0;
|
|
768
|
-
for (Phases p : samples) {
|
|
769
|
-
if (p != null) {
|
|
770
|
-
best = Math.max(best, pick.of(p));
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
return best;
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
/* ================================================================ *
|
|
777
|
-
* 2b. WHY it does not scale — PDFium versus everything else
|
|
778
|
-
* ================================================================ */
|
|
779
|
-
|
|
780
|
-
/**
|
|
781
|
-
* THE DECISIVE EXPERIMENT. Runs the two halves of a tile SEPARATELY across N
|
|
782
|
-
* threads and asks which one refuses to parallelise.
|
|
783
|
-
*
|
|
784
|
-
* A — PDFium only: {@code openPage}, {@code render} into a bitmap allocated
|
|
785
|
-
* once per thread up front, {@code close}. No allocation, no copy, no
|
|
786
|
-
* encode inside the timed region.
|
|
787
|
-
* B — pure Java only: base64 of a 4 MiB {@code byte[]} allocated up front.
|
|
788
|
-
* Same byte volume as one 1024 px tile, no native PDF code anywhere.
|
|
789
|
-
*
|
|
790
|
-
* If A's per-operation cost inflates by roughly N while its wall clock stays
|
|
791
|
-
* flat, every thread was queueing behind one lock — which is what
|
|
792
|
-
* {@link PdfRendererPool}'s own header says AOSP does, and the difference
|
|
793
|
-
* between "the pool buys throughput" and "the pool buys correctness only".
|
|
794
|
-
* B is the control: it is bounded by memory bandwidth and nothing else, so
|
|
795
|
-
* whatever inflation it shows is the floor that any parallel scheme on this
|
|
796
|
-
* phone pays regardless.
|
|
797
|
-
*
|
|
798
|
-
* Reported, never asserted — the presence of the lock is a property of this
|
|
799
|
-
* platform build, and a test that failed when a future Android removed it
|
|
800
|
-
* would be a test that punishes good news.
|
|
801
|
-
*
|
|
802
|
-
* READ THE RATIOS, NOT THE ABSOLUTES. Each round here is one operation per
|
|
803
|
-
* thread with executor churn either side, so the CPU governor never ramps and
|
|
804
|
-
* the scheduler is free to park the work on a little core — measured per-op
|
|
805
|
-
* costs come out 2-4x the tight-loop figures in
|
|
806
|
-
* {@link #phaseBreakdownOfOneTileAtProbeScale} for that reason. A and B are
|
|
807
|
-
* measured identically in the same run, so how each one's THROUGHPUT moves with
|
|
808
|
-
* N is sound even though neither absolute is.
|
|
809
|
-
*/
|
|
810
|
-
@Test
|
|
811
|
-
public void whichPhasesSerialiseUnderConcurrency() throws Exception {
|
|
812
|
-
File pdf = fixture();
|
|
813
|
-
int maxThreads = 4;
|
|
814
|
-
RasterRequest piece = epochPieces(1, 0).get(0);
|
|
815
|
-
int px = piece.pixelWidth();
|
|
816
|
-
|
|
817
|
-
List<ParcelFileDescriptor> fds = new ArrayList<>();
|
|
818
|
-
List<PdfRenderer> renderers = new ArrayList<>();
|
|
819
|
-
List<Bitmap> bitmaps = new ArrayList<>();
|
|
820
|
-
List<byte[]> buffers = new ArrayList<>();
|
|
821
|
-
try {
|
|
822
|
-
for (int i = 0; i < maxThreads; i++) {
|
|
823
|
-
ParcelFileDescriptor pfd =
|
|
824
|
-
ParcelFileDescriptor.open(pdf, ParcelFileDescriptor.MODE_READ_ONLY);
|
|
825
|
-
fds.add(pfd);
|
|
826
|
-
renderers.add(new PdfRenderer(pfd));
|
|
827
|
-
bitmaps.add(Bitmap.createBitmap(px, px, Bitmap.Config.ARGB_8888));
|
|
828
|
-
buffers.add(new byte[px * px * 4]);
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
Matrix matrix = new Matrix();
|
|
832
|
-
matrix.setScale(piece.scale, piece.scale);
|
|
833
|
-
matrix.postTranslate(-piece.docX * piece.scale, -piece.docY * piece.scale);
|
|
834
|
-
|
|
835
|
-
ThreadOp openOnly =
|
|
836
|
-
thread -> {
|
|
837
|
-
long t0 = System.nanoTime();
|
|
838
|
-
renderers.get(thread).openPage(piece.page).close();
|
|
839
|
-
return System.nanoTime() - t0;
|
|
840
|
-
};
|
|
841
|
-
ThreadOp pdfium =
|
|
842
|
-
thread -> {
|
|
843
|
-
long t0 = System.nanoTime();
|
|
844
|
-
PdfRenderer.Page page = renderers.get(thread).openPage(piece.page);
|
|
845
|
-
page.render(
|
|
846
|
-
bitmaps.get(thread), null, matrix, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);
|
|
847
|
-
page.close();
|
|
848
|
-
return System.nanoTime() - t0;
|
|
849
|
-
};
|
|
850
|
-
ThreadOp pureJava =
|
|
851
|
-
thread -> {
|
|
852
|
-
long t0 = System.nanoTime();
|
|
853
|
-
if (Base64.encodeToString(buffers.get(thread), Base64.NO_WRAP).isEmpty()) {
|
|
854
|
-
throw new AssertionError("empty base64");
|
|
855
|
-
}
|
|
856
|
-
return System.nanoTime() - t0;
|
|
857
|
-
};
|
|
858
|
-
|
|
859
|
-
Log.i(
|
|
860
|
-
TAG,
|
|
861
|
-
"SERIALISATION fixture="
|
|
862
|
-
+ (usingRealPdf() ? "REAL " + REAL_PDF : "SYNTHETIC 6-page letter")
|
|
863
|
-
+ " tile="
|
|
864
|
-
+ px
|
|
865
|
-
+ "px@"
|
|
866
|
-
+ PROBE_SCALE
|
|
867
|
-
+ "x A=PDFium(openPage+render+close) B=pure-Java(base64 of "
|
|
868
|
-
+ (px * px * 4)
|
|
869
|
-
+ " B)");
|
|
870
|
-
// Warm every renderer on its own thread first, so no probe round pays for
|
|
871
|
-
// a first-touch page parse and the n=1 baselines are honest.
|
|
872
|
-
probe("warm-up ", maxThreads, pdfium);
|
|
873
|
-
for (int n : new int[] {1, 2, 4}) {
|
|
874
|
-
probe("A0 openPg", n, openOnly);
|
|
875
|
-
probe("A PDFium ", n, pdfium);
|
|
876
|
-
probe("B Java ", n, pureJava);
|
|
877
|
-
}
|
|
878
|
-
} finally {
|
|
879
|
-
for (Bitmap bitmap : bitmaps) {
|
|
880
|
-
bitmap.recycle();
|
|
881
|
-
}
|
|
882
|
-
for (PdfRenderer renderer : renderers) {
|
|
883
|
-
renderer.close();
|
|
884
|
-
}
|
|
885
|
-
for (ParcelFileDescriptor pfd : fds) {
|
|
886
|
-
try {
|
|
887
|
-
pfd.close();
|
|
888
|
-
} catch (IOException ignored) {
|
|
889
|
-
// PdfRenderer.close() already closed it.
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
private interface ThreadOp {
|
|
896
|
-
/** @return nanoseconds spent inside the operation itself, on this thread. */
|
|
897
|
-
long run(int thread) throws Exception;
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
/** Best of five, for the reason {@link #bestOfFive} gives. */
|
|
901
|
-
private static void probe(String label, int n, ThreadOp op) throws Exception {
|
|
902
|
-
long bestWall = Long.MAX_VALUE;
|
|
903
|
-
long bestMedianOp = Long.MAX_VALUE;
|
|
904
|
-
for (int round = 0; round < 5; round++) {
|
|
905
|
-
long[] ops = new long[n];
|
|
906
|
-
ExecutorService exec = Executors.newFixedThreadPool(n);
|
|
907
|
-
CountDownLatch ready = new CountDownLatch(n);
|
|
908
|
-
CountDownLatch go = new CountDownLatch(1);
|
|
909
|
-
CountDownLatch done = new CountDownLatch(n);
|
|
910
|
-
AtomicReference<Throwable> failure = new AtomicReference<>();
|
|
911
|
-
long wall;
|
|
912
|
-
try {
|
|
913
|
-
for (int i = 0; i < n; i++) {
|
|
914
|
-
final int index = i;
|
|
915
|
-
exec.execute(
|
|
916
|
-
() -> {
|
|
917
|
-
try {
|
|
918
|
-
ready.countDown();
|
|
919
|
-
go.await();
|
|
920
|
-
ops[index] = op.run(index);
|
|
921
|
-
} catch (Throwable e) {
|
|
922
|
-
failure.compareAndSet(null, e);
|
|
923
|
-
} finally {
|
|
924
|
-
done.countDown();
|
|
925
|
-
}
|
|
926
|
-
});
|
|
927
|
-
}
|
|
928
|
-
assertTrue("threads did not start in 30s", ready.await(30, TimeUnit.SECONDS));
|
|
929
|
-
long t0 = System.nanoTime();
|
|
930
|
-
go.countDown();
|
|
931
|
-
assertTrue("threads did not finish in 120s", done.await(120, TimeUnit.SECONDS));
|
|
932
|
-
wall = System.nanoTime() - t0;
|
|
933
|
-
} finally {
|
|
934
|
-
exec.shutdownNow();
|
|
935
|
-
}
|
|
936
|
-
Throwable f = failure.get();
|
|
937
|
-
if (f != null) {
|
|
938
|
-
throw new AssertionError("probe " + label + " failed: " + f, f);
|
|
939
|
-
}
|
|
940
|
-
Arrays.sort(ops);
|
|
941
|
-
bestWall = Math.min(bestWall, wall);
|
|
942
|
-
bestMedianOp = Math.min(bestMedianOp, percentile(ops, 50));
|
|
943
|
-
}
|
|
944
|
-
Log.i(
|
|
945
|
-
TAG,
|
|
946
|
-
String.format(
|
|
947
|
-
" %s n=%d wall=%8.3f ms per-op(p50)=%8.3f ms ops/ms=%.3f",
|
|
948
|
-
label, n, bestWall / 1e6, bestMedianOp / 1e6, n / (bestWall / 1e6)));
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
/* ================================================================ *
|
|
952
|
-
* 3. Whole-epoch wall clock — what the user actually waits for
|
|
953
|
-
* ================================================================ */
|
|
954
|
-
|
|
955
|
-
/**
|
|
956
|
-
* The field report's epoch end to end: 6 x 1024 px pieces at 131.25x, rendered
|
|
957
|
-
* the way the controller renders them today (strictly one at a time through a
|
|
958
|
-
* pool of ONE, which is what a single-threaded {@code await} loop reduces the
|
|
959
|
-
* pool to) against the same six across four threads.
|
|
960
|
-
*
|
|
961
|
-
* Reported with the base64 encode included, because that runs on the same
|
|
962
|
-
* worker as the render in {@code PdfCanvasModule} and is therefore part of the
|
|
963
|
-
* serial chain the user waits on.
|
|
964
|
-
*/
|
|
965
|
-
@Test
|
|
966
|
-
public void wholeEpochWallClock() throws Exception {
|
|
967
|
-
File pdf = fixture();
|
|
968
|
-
List<RasterRequest> pieces = epochPieces(6, 0);
|
|
969
|
-
|
|
970
|
-
long serial = bestOfFive(() -> renderSerially(pdf, pieces));
|
|
971
|
-
|
|
972
|
-
PdfCanvasRasterizer r = PdfCanvasRasterizer.openFile(pdf, null, 4);
|
|
973
|
-
long encodeNs = 0;
|
|
974
|
-
int rawBytes = 0;
|
|
975
|
-
long base64Chars = 0;
|
|
976
|
-
try {
|
|
977
|
-
warm(r, pieces, 4);
|
|
978
|
-
for (RasterRequest request : pieces) {
|
|
979
|
-
RasterPixels px = r.render(request, null);
|
|
980
|
-
long t0 = System.nanoTime();
|
|
981
|
-
String encoded = Base64.encodeToString(px.bytes, Base64.NO_WRAP);
|
|
982
|
-
encodeNs += System.nanoTime() - t0;
|
|
983
|
-
rawBytes += px.bytes.length;
|
|
984
|
-
base64Chars += encoded.length();
|
|
985
|
-
}
|
|
986
|
-
} finally {
|
|
987
|
-
r.close();
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
Log.i(
|
|
991
|
-
TAG,
|
|
992
|
-
String.format(
|
|
993
|
-
"EPOCH 6 x %dpx@%.2fx serial render+encode=%d ms of which encode=%.1f ms"
|
|
994
|
-
+ " payload raw=%d B base64=%d chars (%.2f MiB / %.2f MiB)",
|
|
995
|
-
TILE_PX,
|
|
996
|
-
PROBE_SCALE,
|
|
997
|
-
serial,
|
|
998
|
-
encodeNs / 1e6,
|
|
999
|
-
rawBytes,
|
|
1000
|
-
base64Chars,
|
|
1001
|
-
rawBytes / (1024.0 * 1024.0),
|
|
1002
|
-
base64Chars / (1024.0 * 1024.0)));
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
/* ================================================================ *
|
|
1006
|
-
* 5. The controller's lane scheduler, before and after
|
|
1007
|
-
* ================================================================ */
|
|
1008
|
-
|
|
1009
|
-
/**
|
|
1010
|
-
* BEFORE vs AFTER for the change that replaced the controller's sequential
|
|
1011
|
-
* {@code for ... await handle.render(...)} loop with
|
|
1012
|
-
* {@code maxConcurrentRenders} lanes.
|
|
1013
|
-
*
|
|
1014
|
-
* This is the same shape as {@link #poolConcurrencyScalingAtProbeScale} with
|
|
1015
|
-
* one difference that matters: that test gives every thread its own piece,
|
|
1016
|
-
* which is a fan-out, whereas the controller runs LANES — N workers pulling
|
|
1017
|
-
* from one shared queue of plan items until it is empty. With 6 pieces and 2
|
|
1018
|
-
* lanes those are not the same schedule, and the shipped code is the lane one,
|
|
1019
|
-
* so it is the one measured here.
|
|
1020
|
-
*
|
|
1021
|
-
* WHAT THIS NUMBER IS, AND — MORE IMPORTANTLY — WHAT IT IS NOT. It is the
|
|
1022
|
-
* whole JAVA side of a 6-piece detail epoch: {@code openPage}, the bitmap, the
|
|
1023
|
-
* white fill, {@code PdfRenderer.render}, the copy to a {@code byte[]}, and the
|
|
1024
|
-
* base64 encode, exactly as {@code PdfCanvasModule} performs them on its render
|
|
1025
|
-
* executor. It is NOT the epoch the user waits for. The field probe timed one
|
|
1026
|
-
* tile at 595 ms while the whole Java side of that tile is 27.5 ms p50, so
|
|
1027
|
-
* roughly 95% of what the user waits for is the transport — a 5,592,408-char
|
|
1028
|
-
* base64 string crossing the bridge and being decoded one {@code charCodeAt} at
|
|
1029
|
-
* a time on the JS thread — and none of that exists in a process with no React
|
|
1030
|
-
* Native in it. Lanes cannot overlap the JS half with itself; the JS thread is
|
|
1031
|
-
* one thread. So the honest end-to-end expectation is the SMALL number this
|
|
1032
|
-
* measures, not a multiple of it, and the 4x has to come out of the transport.
|
|
1033
|
-
*/
|
|
1034
|
-
@Test
|
|
1035
|
-
public void laneSchedulerEpochWallClock() throws Exception {
|
|
1036
|
-
File pdf = fixture();
|
|
1037
|
-
List<RasterRequest> pieces = epochPieces(6, 0);
|
|
1038
|
-
|
|
1039
|
-
Log.i(
|
|
1040
|
-
TAG,
|
|
1041
|
-
"LANES fixture="
|
|
1042
|
-
+ (usingRealPdf() ? "REAL " + REAL_PDF : "SYNTHETIC 6-page letter")
|
|
1043
|
-
+ " pieces="
|
|
1044
|
-
+ pieces.size()
|
|
1045
|
-
+ " tile="
|
|
1046
|
-
+ TILE_PX
|
|
1047
|
-
+ "px scale="
|
|
1048
|
-
+ PROBE_SCALE);
|
|
1049
|
-
|
|
1050
|
-
long before = bestOfFive(() -> renderInLanes(pdf, pieces, 1));
|
|
1051
|
-
for (int lanes : new int[] {2, 3, 4}) {
|
|
1052
|
-
long after = bestOfFive(() -> renderInLanes(pdf, pieces, lanes));
|
|
1053
|
-
Log.i(
|
|
1054
|
-
TAG,
|
|
1055
|
-
String.format(
|
|
1056
|
-
"LANES before(1 lane)=%4d ms after(%d lanes)=%4d ms speedup=%.2fx"
|
|
1057
|
-
+ " [JAVA SIDE ONLY — see javadoc]",
|
|
1058
|
-
before, lanes, after, before / (double) Math.max(1L, after)));
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
laneScheduleOnTheJsiTransport(pdf, pieces);
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
/**
|
|
1065
|
-
* THE SAME LANE SCHEDULE WITH THE BASE64 ENCODE DELETED, which is what the JSI
|
|
1066
|
-
* transport actually ships.
|
|
1067
|
-
*
|
|
1068
|
-
* <h3>Why the rows above do not transfer</h3>
|
|
1069
|
-
*
|
|
1070
|
-
* Every lane win in the base64 block comes from spreading the ENCODE — 75-80%
|
|
1071
|
-
* of the Java cost of a 1024 px tile, and the one phase in it that is pure Java
|
|
1072
|
-
* and therefore genuinely parallel (see
|
|
1073
|
-
* {@link #whichPhasesSerialiseUnderConcurrency}: {@code openPage} and
|
|
1074
|
-
* {@code render} are flat-to-convoying across threads while a pure-Java control
|
|
1075
|
-
* scales 2.8x). On the JSI path that phase is gone: the worker moves the raster
|
|
1076
|
-
* into C++-owned memory and the promise resolves with a slot number. So the
|
|
1077
|
-
* parallel fraction of a tile collapses, and the lane question has to be asked
|
|
1078
|
-
* again against the work that is left.
|
|
1079
|
-
*
|
|
1080
|
-
* <h3>TWO ROWS, and the difference between them is the change under test</h3>
|
|
1081
|
-
*
|
|
1082
|
-
* {@code LANES-JSI} is the two-copy store: {@code render()} produces a
|
|
1083
|
-
* {@code byte[]} and {@link PdfCanvasPixelBridge#nativeStore} copies it into
|
|
1084
|
-
* the slot. {@code LANES-JSI-DIRECT} is
|
|
1085
|
-
* {@link PdfCanvasRasterizer#renderToNativeSlot}, which fills the slot from the
|
|
1086
|
-
* {@code Bitmap} and never allocates the array at all — one full-raster ART
|
|
1087
|
-
* allocation and one full-raster memcpy less, per tile.
|
|
1088
|
-
*
|
|
1089
|
-
* <p>THE DELTA IS THE POINT, not the absolute numbers, and it is a per-tile
|
|
1090
|
-
* delta on the render executor, which
|
|
1091
|
-
* {@link #whichPhasesSerialiseUnderConcurrency} shows is the serialised
|
|
1092
|
-
* bottleneck — so it does not spread across lanes and does not shrink as lanes
|
|
1093
|
-
* are added. Read {@code ms/tile at p50} between the two blocks at MATCHED lane
|
|
1094
|
-
* counts; comparing a 4-lane DIRECT row against a 1-lane JSI row measures the
|
|
1095
|
-
* lane count instead.
|
|
1096
|
-
*
|
|
1097
|
-
* <p>Both are measured in the SAME interleaved schedule for the reason the next
|
|
1098
|
-
* paragraph gives: run one block to completion and then the other, and the
|
|
1099
|
-
* second one is charged for the first one heating the phone.
|
|
1100
|
-
*
|
|
1101
|
-
* <h3>Why the lane counts are interleaved rather than measured in a row</h3>
|
|
1102
|
-
*
|
|
1103
|
-
* The phone heats up and the governor moves, so measuring 1 lane five times and
|
|
1104
|
-
* then 4 lanes five times charges the later block for the earlier one. Each
|
|
1105
|
-
* round runs every lane count once, and the round number rotates which goes
|
|
1106
|
-
* first, so no lane count is systematically on a cold or a hot phone.
|
|
1107
|
-
*
|
|
1108
|
-
* <h3>Why min AND p50 AND max</h3>
|
|
1109
|
-
*
|
|
1110
|
-
* A single best-of-N cannot say whether two lane counts genuinely differ or
|
|
1111
|
-
* merely sit inside each other spread, and that is exactly the verdict this
|
|
1112
|
-
* measurement exists to support.
|
|
1113
|
-
*/
|
|
1114
|
-
private static void laneScheduleOnTheJsiTransport(File pdf, List<RasterRequest> pieces)
|
|
1115
|
-
throws Exception {
|
|
1116
|
-
assertTrue(
|
|
1117
|
-
"libpdfcanvasjsi.so did not load, so there is no JSI path to measure.",
|
|
1118
|
-
PdfCanvasPixelBridge.isLoaded());
|
|
1119
|
-
|
|
1120
|
-
final int[] laneCounts = {1, 2, 3, 4};
|
|
1121
|
-
final int rounds = 21;
|
|
1122
|
-
long[][] store = new long[laneCounts.length][rounds];
|
|
1123
|
-
long[][] direct = new long[laneCounts.length][rounds];
|
|
1124
|
-
for (int round = 0; round < rounds; round++) {
|
|
1125
|
-
for (int k = 0; k < laneCounts.length; k++) {
|
|
1126
|
-
int idx = (k + round) % laneCounts.length;
|
|
1127
|
-
// Both fills at every lane count in every round, and which one goes
|
|
1128
|
-
// first alternates. Otherwise whichever is second is systematically
|
|
1129
|
-
// measured on a hotter phone, which is a bias the size of the effect.
|
|
1130
|
-
if ((round & 1) == 0) {
|
|
1131
|
-
store[idx][round] = renderInLanesOverJsi(pdf, pieces, laneCounts[idx], false);
|
|
1132
|
-
direct[idx][round] = renderInLanesOverJsi(pdf, pieces, laneCounts[idx], true);
|
|
1133
|
-
} else {
|
|
1134
|
-
direct[idx][round] = renderInLanesOverJsi(pdf, pieces, laneCounts[idx], true);
|
|
1135
|
-
store[idx][round] = renderInLanesOverJsi(pdf, pieces, laneCounts[idx], false);
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
reportLaneBlock("LANES-JSI", store, laneCounts, pieces.size());
|
|
1141
|
-
reportLaneBlock("LANES-JSI-DIRECT", direct, laneCounts, pieces.size());
|
|
1142
|
-
|
|
1143
|
-
// The number the decision rests on: same lane count, both fills, per tile.
|
|
1144
|
-
for (int k = 0; k < laneCounts.length; k++) {
|
|
1145
|
-
long[] a = store[k].clone();
|
|
1146
|
-
long[] b = direct[k].clone();
|
|
1147
|
-
Arrays.sort(a);
|
|
1148
|
-
Arrays.sort(b);
|
|
1149
|
-
double perTileStore = percentile(a, 50) / (double) pieces.size();
|
|
1150
|
-
double perTileDirect = percentile(b, 50) / (double) pieces.size();
|
|
1151
|
-
Log.i(
|
|
1152
|
-
TAG,
|
|
1153
|
-
String.format(
|
|
1154
|
-
"LANES-JSI-DELTA lanes=%d store=%5.2f ms/tile direct=%5.2f ms/tile"
|
|
1155
|
-
+ " saved=%5.2f ms/tile (%+.1f%%) [p50, matched lanes]",
|
|
1156
|
-
laneCounts[k],
|
|
1157
|
-
perTileStore,
|
|
1158
|
-
perTileDirect,
|
|
1159
|
-
perTileStore - perTileDirect,
|
|
1160
|
-
perTileStore == 0 ? 0 : -100.0 * (perTileStore - perTileDirect) / perTileStore));
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
private static void reportLaneBlock(
|
|
1165
|
-
String label, long[][] runs, int[] laneCounts, int pieceCount) {
|
|
1166
|
-
long[] baseline = runs[0].clone();
|
|
1167
|
-
Arrays.sort(baseline);
|
|
1168
|
-
for (int k = 0; k < laneCounts.length; k++) {
|
|
1169
|
-
long[] sorted = runs[k].clone();
|
|
1170
|
-
Arrays.sort(sorted);
|
|
1171
|
-
Log.i(
|
|
1172
|
-
TAG,
|
|
1173
|
-
String.format(
|
|
1174
|
-
"%s lanes=%d min=%4d ms p50=%4d ms max=%4d ms"
|
|
1175
|
-
+ " mean=%6.1f ms (%5.1f ms/tile at p50)"
|
|
1176
|
-
+ " vs 1 lane: min=%.2fx p50=%.2fx mean=%.2fx",
|
|
1177
|
-
label,
|
|
1178
|
-
laneCounts[k],
|
|
1179
|
-
sorted[0],
|
|
1180
|
-
percentile(sorted, 50),
|
|
1181
|
-
sorted[sorted.length - 1],
|
|
1182
|
-
mean(sorted),
|
|
1183
|
-
percentile(sorted, 50) / (double) pieceCount,
|
|
1184
|
-
baseline[0] / (double) Math.max(1L, sorted[0]),
|
|
1185
|
-
percentile(baseline, 50) / (double) Math.max(1L, percentile(sorted, 50)),
|
|
1186
|
-
mean(baseline) / Math.max(1.0, mean(sorted))));
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
/**
|
|
1191
|
-
* The controller's schedule, in Java: {@code lanes} workers pulling plan items
|
|
1192
|
-
* off one shared cursor until it is empty, each doing the full production
|
|
1193
|
-
* per-tile work. A pool of {@code min(lanes, MAX_POOL_SIZE)}, because that is
|
|
1194
|
-
* what the rasterizer now declares as {@code maxConcurrentRenders}.
|
|
1195
|
-
*/
|
|
1196
|
-
private static long renderInLanes(File pdf, List<RasterRequest> pieces, int lanes)
|
|
1197
|
-
throws Exception {
|
|
1198
|
-
int poolSize = Math.min(lanes, PdfRendererPool.MAX_POOL_SIZE);
|
|
1199
|
-
PdfCanvasRasterizer r = PdfCanvasRasterizer.openFile(pdf, null, poolSize);
|
|
1200
|
-
ExecutorService exec = Executors.newFixedThreadPool(lanes);
|
|
1201
|
-
CountDownLatch ready = new CountDownLatch(lanes);
|
|
1202
|
-
CountDownLatch go = new CountDownLatch(1);
|
|
1203
|
-
CountDownLatch done = new CountDownLatch(lanes);
|
|
1204
|
-
AtomicInteger cursor = new AtomicInteger(0);
|
|
1205
|
-
AtomicReference<Throwable> failure = new AtomicReference<>();
|
|
1206
|
-
long t0;
|
|
1207
|
-
try {
|
|
1208
|
-
warm(r, pieces, poolSize);
|
|
1209
|
-
for (int i = 0; i < lanes; i++) {
|
|
1210
|
-
exec.execute(
|
|
1211
|
-
() -> {
|
|
1212
|
-
try {
|
|
1213
|
-
ready.countDown();
|
|
1214
|
-
go.await();
|
|
1215
|
-
for (; ; ) {
|
|
1216
|
-
int slot = cursor.getAndIncrement();
|
|
1217
|
-
if (slot >= pieces.size()) {
|
|
1218
|
-
return;
|
|
1219
|
-
}
|
|
1220
|
-
renderAndEncode(r, pieces.get(slot));
|
|
1221
|
-
}
|
|
1222
|
-
} catch (Throwable e) {
|
|
1223
|
-
failure.compareAndSet(null, e);
|
|
1224
|
-
} finally {
|
|
1225
|
-
done.countDown();
|
|
1226
|
-
}
|
|
1227
|
-
});
|
|
1228
|
-
}
|
|
1229
|
-
assertTrue("lanes did not start in 30s", ready.await(30, TimeUnit.SECONDS));
|
|
1230
|
-
t0 = System.nanoTime();
|
|
1231
|
-
go.countDown();
|
|
1232
|
-
assertTrue("lanes did not finish in 120s", done.await(120, TimeUnit.SECONDS));
|
|
1233
|
-
} finally {
|
|
1234
|
-
exec.shutdownNow();
|
|
1235
|
-
r.close();
|
|
1236
|
-
}
|
|
1237
|
-
Throwable f = failure.get();
|
|
1238
|
-
if (f != null) {
|
|
1239
|
-
throw new AssertionError("a lane failed: " + f, f);
|
|
1240
|
-
}
|
|
1241
|
-
return (System.nanoTime() - t0) / 1_000_000L;
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
/**
|
|
1245
|
-
* The identical lane schedule with the production JSI per-tile work.
|
|
1246
|
-
*
|
|
1247
|
-
* {@code PdfCanvasModule.render} on the fast transport does exactly two things
|
|
1248
|
-
* per tile on its render worker: the raster, and
|
|
1249
|
-
* {@code PdfCanvasPixelBridge.nativeStore} of the finished {@code byte[]} into
|
|
1250
|
-
* C++-owned memory. There is no string and no encode. Both are inside the clock
|
|
1251
|
-
* because both are on the worker.
|
|
1252
|
-
*
|
|
1253
|
-
* WHAT IS DELIBERATELY OUTSIDE THE CLOCK: freeing the slots. In production the
|
|
1254
|
-
* buffer is handed to JS as an ArrayBuffer and freed when that is collected, so
|
|
1255
|
-
* the render worker never pays for the free — charging this measurement for it
|
|
1256
|
-
* would invent a cost the shipped path does not have. The slots are drained
|
|
1257
|
-
* after the clock stops instead, which is also what stops a run leaving 24 MiB
|
|
1258
|
-
* of native memory behind for the next one.
|
|
1259
|
-
*/
|
|
1260
|
-
private static long renderInLanesOverJsi(
|
|
1261
|
-
File pdf, List<RasterRequest> pieces, int lanes, boolean direct) throws Exception {
|
|
1262
|
-
int poolSize = Math.min(lanes, PdfRendererPool.MAX_POOL_SIZE);
|
|
1263
|
-
PdfCanvasRasterizer r = PdfCanvasRasterizer.openFile(pdf, null, poolSize);
|
|
1264
|
-
ExecutorService exec = Executors.newFixedThreadPool(lanes);
|
|
1265
|
-
CountDownLatch ready = new CountDownLatch(lanes);
|
|
1266
|
-
CountDownLatch go = new CountDownLatch(1);
|
|
1267
|
-
CountDownLatch done = new CountDownLatch(lanes);
|
|
1268
|
-
AtomicInteger cursor = new AtomicInteger(0);
|
|
1269
|
-
AtomicReference<Throwable> failure = new AtomicReference<>();
|
|
1270
|
-
// Every lane stores; the drain below happens on this thread once they are all
|
|
1271
|
-
// finished, so the queue is the only shared mutable thing between them.
|
|
1272
|
-
ConcurrentLinkedQueue<Long> parked = new ConcurrentLinkedQueue<>();
|
|
1273
|
-
long elapsedMs;
|
|
1274
|
-
long t0;
|
|
1275
|
-
try {
|
|
1276
|
-
warm(r, pieces, poolSize);
|
|
1277
|
-
for (int i = 0; i < lanes; i++) {
|
|
1278
|
-
exec.execute(
|
|
1279
|
-
() -> {
|
|
1280
|
-
try {
|
|
1281
|
-
ready.countDown();
|
|
1282
|
-
go.await();
|
|
1283
|
-
for (; ; ) {
|
|
1284
|
-
int slot = cursor.getAndIncrement();
|
|
1285
|
-
if (slot >= pieces.size()) {
|
|
1286
|
-
return;
|
|
1287
|
-
}
|
|
1288
|
-
if (direct) {
|
|
1289
|
-
renderAndFillDirect(r, pieces.get(slot), parked);
|
|
1290
|
-
} else {
|
|
1291
|
-
renderAndStore(r, pieces.get(slot), parked);
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
} catch (Throwable e) {
|
|
1295
|
-
failure.compareAndSet(null, e);
|
|
1296
|
-
} finally {
|
|
1297
|
-
done.countDown();
|
|
1298
|
-
}
|
|
1299
|
-
});
|
|
1300
|
-
}
|
|
1301
|
-
assertTrue("lanes did not start in 30s", ready.await(30, TimeUnit.SECONDS));
|
|
1302
|
-
t0 = System.nanoTime();
|
|
1303
|
-
go.countDown();
|
|
1304
|
-
assertTrue("lanes did not finish in 120s", done.await(120, TimeUnit.SECONDS));
|
|
1305
|
-
elapsedMs = (System.nanoTime() - t0) / 1_000_000L;
|
|
1306
|
-
} finally {
|
|
1307
|
-
exec.shutdownNow();
|
|
1308
|
-
r.close();
|
|
1309
|
-
for (Long slot : parked) {
|
|
1310
|
-
PdfCanvasPixelBridge.nativeRelease(slot);
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
Throwable f = failure.get();
|
|
1314
|
-
if (f != null) {
|
|
1315
|
-
throw new AssertionError("a lane failed: " + f, f);
|
|
1316
|
-
}
|
|
1317
|
-
return elapsedMs;
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
/**
|
|
1321
|
-
* The Java-side work of ONE tile on the two-copy JSI store — what
|
|
1322
|
-
* {@code PdfCanvasModule} did before the one-copy fill replaced it.
|
|
1323
|
-
*
|
|
1324
|
-
* The counterpart of {@link #renderAndEncode}, and the difference between the
|
|
1325
|
-
* two IS the change being measured.
|
|
1326
|
-
*/
|
|
1327
|
-
private static void renderAndStore(
|
|
1328
|
-
PdfCanvasRasterizer r, RasterRequest request, ConcurrentLinkedQueue<Long> parked)
|
|
1329
|
-
throws PdfRasterException {
|
|
1330
|
-
RasterPixels pixels = r.render(request, null);
|
|
1331
|
-
long slot = JSI_SLOT.getAndIncrement();
|
|
1332
|
-
PdfCanvasPixelBridge.nativeStore(slot, pixels.bytes);
|
|
1333
|
-
parked.add(slot);
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
/**
|
|
1337
|
-
* The Java-side work of ONE tile on the ONE-COPY fill, as
|
|
1338
|
-
* {@code PdfCanvasModule.render} performs it today.
|
|
1339
|
-
*
|
|
1340
|
-
* The counterpart of {@link #renderAndStore}: identical except that the raster
|
|
1341
|
-
* is written into the slot from the {@code Bitmap} instead of into a
|
|
1342
|
-
* {@code byte[]} and then into the slot. Everything else — the request, the
|
|
1343
|
-
* pool, the lane schedule, the deliberate exclusion of the free — is held
|
|
1344
|
-
* constant, so the difference between the two rows is one full-raster ART
|
|
1345
|
-
* allocation plus one full-raster memcpy and nothing else.
|
|
1346
|
-
*
|
|
1347
|
-
* A refusal is a FAILURE here, not a fallback. In production it degrades to
|
|
1348
|
-
* base64 and the tile is still correct, but a measurement that quietly
|
|
1349
|
-
* switched to the path it was comparing against would report the two rows as
|
|
1350
|
-
* identical and call the change worthless.
|
|
1351
|
-
*/
|
|
1352
|
-
private static void renderAndFillDirect(
|
|
1353
|
-
PdfCanvasRasterizer r, RasterRequest request, ConcurrentLinkedQueue<Long> parked)
|
|
1354
|
-
throws PdfRasterException {
|
|
1355
|
-
long slot = JSI_SLOT.getAndIncrement();
|
|
1356
|
-
RasterPixels pixels = r.renderToNativeSlot(request, null, slot);
|
|
1357
|
-
if (!pixels.isInNativeSlot()) {
|
|
1358
|
-
throw new AssertionError(
|
|
1359
|
-
"the direct fill refused and fell back to the heap, so this sample is "
|
|
1360
|
-
+ "measuring the path it is supposed to be compared against");
|
|
1361
|
-
}
|
|
1362
|
-
parked.add(slot);
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
/**
|
|
1366
|
-
* Slot ids for this file, in a range of its own.
|
|
1367
|
-
*
|
|
1368
|
-
* {@code PdfCanvasJsiTransportTest} owns 1_000_000 upward and drains every slot
|
|
1369
|
-
* in an {@code @Before}, so one shared counter would let whichever class ran
|
|
1370
|
-
* second reuse a live id.
|
|
1371
|
-
*/
|
|
1372
|
-
private static final AtomicLong JSI_SLOT = new AtomicLong(9_000_000L);
|
|
1373
|
-
|
|
1374
|
-
/* ================================================================ */
|
|
1375
|
-
|
|
1376
|
-
private static double mean(long[] values) {
|
|
1377
|
-
long sum = 0;
|
|
1378
|
-
for (long value : values) {
|
|
1379
|
-
sum += value;
|
|
1380
|
-
}
|
|
1381
|
-
return sum / (double) values.length;
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
|
-
private static long percentile(long[] sorted, int p) {
|
|
1385
|
-
int rank = (int) Math.ceil((p / 100.0) * sorted.length) - 1;
|
|
1386
|
-
return sorted[Math.max(0, Math.min(sorted.length - 1, rank))];
|
|
1387
|
-
}
|
|
1388
|
-
}
|