@reekon-tools/react-native-pdf-canvas 0.1.1 → 0.2.1
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 +77 -40
- package/README.md +218 -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/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/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 +27 -7
- package/react-native.config.js +5 -2
- package/scripts/fetch-pdfium.mjs +559 -0
- package/scripts/pdfium-manifest.json +48 -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,226 +0,0 @@
|
|
|
1
|
-
package tools.reekon.pdfcanvas;
|
|
2
|
-
|
|
3
|
-
import static org.junit.Assert.assertEquals;
|
|
4
|
-
import static org.junit.Assert.assertNotNull;
|
|
5
|
-
import static org.junit.Assert.assertNull;
|
|
6
|
-
import static org.junit.Assert.assertTrue;
|
|
7
|
-
import static org.junit.Assert.fail;
|
|
8
|
-
|
|
9
|
-
import android.content.Context;
|
|
10
|
-
import android.util.Log;
|
|
11
|
-
|
|
12
|
-
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
13
|
-
import androidx.test.platform.app.InstrumentationRegistry;
|
|
14
|
-
|
|
15
|
-
import org.junit.Test;
|
|
16
|
-
import org.junit.runner.RunWith;
|
|
17
|
-
|
|
18
|
-
import java.io.File;
|
|
19
|
-
import java.util.concurrent.CountDownLatch;
|
|
20
|
-
import java.util.concurrent.TimeUnit;
|
|
21
|
-
import java.util.concurrent.atomic.AtomicReference;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Cancellation, and specifically the HONESTY of it.
|
|
25
|
-
*
|
|
26
|
-
* {@code PdfRenderer.Page.render} is synchronous and uninterruptible once
|
|
27
|
-
* entered. The claim this backend makes is therefore narrow: a cancel prevents
|
|
28
|
-
* the NEXT piece of work starting and discards a result that is no longer
|
|
29
|
-
* wanted, and it does not — cannot — stop a render that is already drawing.
|
|
30
|
-
*
|
|
31
|
-
* A test that only asserted "cancelling makes it throw" would pass equally for a
|
|
32
|
-
* backend that genuinely interrupted and for one that pretends to. The
|
|
33
|
-
* distinguishing evidence is WHICH STAGE reported the cancellation, which is why
|
|
34
|
-
* {@link RasterCancellation} puts the stage in the message.
|
|
35
|
-
*/
|
|
36
|
-
@RunWith(AndroidJUnit4.class)
|
|
37
|
-
public class PdfCanvasCancellationTest {
|
|
38
|
-
|
|
39
|
-
private static final String TAG = "PDFCANVAS";
|
|
40
|
-
|
|
41
|
-
private static Context ctx() {
|
|
42
|
-
return InstrumentationRegistry.getInstrumentation().getTargetContext();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* CATCHES: a signal that is only consulted after the work is done — i.e. an
|
|
47
|
-
* implementation that renders first and checks later. The bar here is not just
|
|
48
|
-
* the error code but that no bitmap was allocated and no page opened, which
|
|
49
|
-
* shows up as this call returning far faster than a real render.
|
|
50
|
-
*/
|
|
51
|
-
@Test
|
|
52
|
-
public void cancelBeforeStartRefusesWithoutRendering() throws Exception {
|
|
53
|
-
File pdf = TestPdfs.pattern(ctx(), "cancel-archd.pdf", TestPdfs.ARCH_D_W, TestPdfs.ARCH_D_H);
|
|
54
|
-
PdfCanvasRasterizer r = PdfCanvasRasterizer.openFile(pdf, null, 1);
|
|
55
|
-
try {
|
|
56
|
-
RasterRequest heavy =
|
|
57
|
-
new RasterRequest(
|
|
58
|
-
0, 200f, 300f, 1200f, 1200f, 1f, false, RasterRequest.BACKGROUND_WHITE);
|
|
59
|
-
|
|
60
|
-
long t0 = System.nanoTime();
|
|
61
|
-
r.render(heavy, null);
|
|
62
|
-
long uncancelledMs = (System.nanoTime() - t0) / 1_000_000;
|
|
63
|
-
|
|
64
|
-
RasterCancellation signal = new RasterCancellation();
|
|
65
|
-
signal.cancel();
|
|
66
|
-
long t1 = System.nanoTime();
|
|
67
|
-
try {
|
|
68
|
-
r.render(heavy, signal);
|
|
69
|
-
fail("A pre-cancelled render must not return pixels");
|
|
70
|
-
} catch (PdfRasterException e) {
|
|
71
|
-
long cancelledMs = (System.nanoTime() - t1) / 1_000_000;
|
|
72
|
-
assertEquals(PdfErrorCode.CANCELLED, e.code());
|
|
73
|
-
assertTrue(
|
|
74
|
-
"should have reported the entry check, not a later stage: " + e.getMessage(),
|
|
75
|
-
e.getMessage().contains("before start"));
|
|
76
|
-
Log.i(TAG, "cancel-before-start: uncancelled=" + uncancelledMs + "ms refused in "
|
|
77
|
-
+ cancelledMs + "ms");
|
|
78
|
-
assertTrue(
|
|
79
|
-
"refusing should be far cheaper than rendering (uncancelled=" + uncancelledMs
|
|
80
|
-
+ "ms, refused=" + cancelledMs + "ms)",
|
|
81
|
-
cancelledMs * 4 < uncancelledMs || cancelledMs <= 2);
|
|
82
|
-
}
|
|
83
|
-
} finally {
|
|
84
|
-
r.close();
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* CATCHES: a backend that CLAIMS to interrupt a render.
|
|
90
|
-
*
|
|
91
|
-
* A cancel raised while {@code PdfRenderer.render} is executing cannot take
|
|
92
|
-
* effect until it returns, so the only stage that can report it is the
|
|
93
|
-
* post-render check — the one that saves no drawing and merely stops a
|
|
94
|
-
* superseded raster being handed back. If some future change chunked the
|
|
95
|
-
* render and bailed early, this assertion would fail, which is the point:
|
|
96
|
-
* the claim in {@link RasterCancellation}'s comment and the code would have
|
|
97
|
-
* diverged.
|
|
98
|
-
*
|
|
99
|
-
* The retry loop is not flakiness padding — it is how the test lands the
|
|
100
|
-
* cancel INSIDE the render window on machines whose render time it does not
|
|
101
|
-
* know. A cancel that arrives too early is reported by the "before render"
|
|
102
|
-
* stage and a cancel that arrives too late does not fire at all; both are
|
|
103
|
-
* detected by name and retried with an adjusted delay, and neither is accepted
|
|
104
|
-
* as a pass.
|
|
105
|
-
*/
|
|
106
|
-
@Test
|
|
107
|
-
public void cancelDuringAnInFlightRenderIsObservedOnlyAfterItFinishes() throws Exception {
|
|
108
|
-
File pdf = TestPdfs.pattern(ctx(), "cancel2-archd.pdf", TestPdfs.ARCH_D_W, TestPdfs.ARCH_D_H);
|
|
109
|
-
PdfCanvasRasterizer r = PdfCanvasRasterizer.openFile(pdf, null, 1);
|
|
110
|
-
try {
|
|
111
|
-
// Big enough that the render window is measurable. S1 finding 2: cost
|
|
112
|
-
// tracks DESTINATION BITMAP AREA, so 1600x1600 is the lever, not the
|
|
113
|
-
// scale.
|
|
114
|
-
RasterRequest heavy =
|
|
115
|
-
new RasterRequest(
|
|
116
|
-
0, 200f, 300f, 800f, 800f, 2f, false, RasterRequest.BACKGROUND_WHITE);
|
|
117
|
-
|
|
118
|
-
long baselineMs = Long.MAX_VALUE;
|
|
119
|
-
for (int i = 0; i < 3; i++) {
|
|
120
|
-
long t = System.nanoTime();
|
|
121
|
-
r.render(heavy, null);
|
|
122
|
-
baselineMs = Math.min(baselineMs, (System.nanoTime() - t) / 1_000_000);
|
|
123
|
-
}
|
|
124
|
-
Log.i(TAG, "cancel-in-flight: baseline render = " + baselineMs + "ms");
|
|
125
|
-
|
|
126
|
-
long delayMs = Math.max(5, baselineMs / 2);
|
|
127
|
-
String stage = null;
|
|
128
|
-
long elapsedMs = -1;
|
|
129
|
-
|
|
130
|
-
for (int attempt = 0; attempt < 6 && stage == null; attempt++) {
|
|
131
|
-
CountDownLatch started = new CountDownLatch(1);
|
|
132
|
-
AtomicReference<PdfRasterException> thrown = new AtomicReference<>();
|
|
133
|
-
AtomicReference<RasterPixels> returned = new AtomicReference<>();
|
|
134
|
-
RasterCancellation signal = new RasterCancellation();
|
|
135
|
-
long[] elapsed = new long[1];
|
|
136
|
-
|
|
137
|
-
Thread worker =
|
|
138
|
-
new Thread(
|
|
139
|
-
() -> {
|
|
140
|
-
started.countDown();
|
|
141
|
-
long t = System.nanoTime();
|
|
142
|
-
try {
|
|
143
|
-
returned.set(r.render(heavy, signal));
|
|
144
|
-
} catch (PdfRasterException e) {
|
|
145
|
-
thrown.set(e);
|
|
146
|
-
}
|
|
147
|
-
elapsed[0] = (System.nanoTime() - t) / 1_000_000;
|
|
148
|
-
});
|
|
149
|
-
worker.start();
|
|
150
|
-
assertTrue(started.await(10, TimeUnit.SECONDS));
|
|
151
|
-
Thread.sleep(delayMs);
|
|
152
|
-
signal.cancel();
|
|
153
|
-
worker.join(120_000);
|
|
154
|
-
|
|
155
|
-
PdfRasterException e = thrown.get();
|
|
156
|
-
if (e == null) {
|
|
157
|
-
// Cancel landed after the render had already returned. Aim earlier.
|
|
158
|
-
assertNotNull("neither a result nor an exception", returned.get());
|
|
159
|
-
delayMs = Math.max(2, delayMs / 2);
|
|
160
|
-
Log.i(TAG, "cancel-in-flight: attempt " + attempt + " landed too late, retrying at "
|
|
161
|
-
+ delayMs + "ms");
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
assertEquals(PdfErrorCode.CANCELLED, e.code());
|
|
165
|
-
if (e.getMessage().contains("after render")) {
|
|
166
|
-
stage = "after render";
|
|
167
|
-
elapsedMs = elapsed[0];
|
|
168
|
-
} else {
|
|
169
|
-
// Landed before render() was entered. Aim later.
|
|
170
|
-
delayMs = Math.max(delayMs * 2, delayMs + 5);
|
|
171
|
-
Log.i(TAG, "cancel-in-flight: attempt " + attempt + " landed at '" + e.getMessage()
|
|
172
|
-
+ "', retrying at " + delayMs + "ms");
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
assertNotNull(
|
|
177
|
-
"never managed to cancel inside the render window; baseline=" + baselineMs + "ms",
|
|
178
|
-
stage);
|
|
179
|
-
Log.i(TAG, "cancel-in-flight: observed at '" + stage + "' after " + elapsedMs
|
|
180
|
-
+ "ms (baseline " + baselineMs + "ms)");
|
|
181
|
-
// The render ran to completion despite the cancel. If it had been
|
|
182
|
-
// interrupted, this would be a fraction of the baseline.
|
|
183
|
-
assertTrue(
|
|
184
|
-
"a cancelled in-flight render should still have taken about a full render "
|
|
185
|
-
+ "(elapsed=" + elapsedMs + "ms, baseline=" + baselineMs + "ms)",
|
|
186
|
-
elapsedMs * 2 >= baselineMs);
|
|
187
|
-
} finally {
|
|
188
|
-
r.close();
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* CATCHES: a cancel that leaks across requests — a shared or static signal, or
|
|
194
|
-
* a pool slot left in a cancelled state. One superseded tile must not poison
|
|
195
|
-
* the next one, and with a pool of one the two renders provably share a
|
|
196
|
-
* renderer.
|
|
197
|
-
*/
|
|
198
|
-
@Test
|
|
199
|
-
public void aCancelledRenderDoesNotAffectTheNextOne() throws Exception {
|
|
200
|
-
File pdf = TestPdfs.quadrants(ctx(), "cancel-quads.pdf", 200, 300);
|
|
201
|
-
PdfCanvasRasterizer r = PdfCanvasRasterizer.openFile(pdf, null, 1);
|
|
202
|
-
try {
|
|
203
|
-
RasterRequest request =
|
|
204
|
-
new RasterRequest(0, 0f, 0f, 200f, 300f, 1f, false, RasterRequest.BACKGROUND_WHITE);
|
|
205
|
-
|
|
206
|
-
RasterCancellation cancelled = new RasterCancellation();
|
|
207
|
-
cancelled.cancel();
|
|
208
|
-
try {
|
|
209
|
-
r.render(request, cancelled);
|
|
210
|
-
fail("expected the cancelled render to throw");
|
|
211
|
-
} catch (PdfRasterException expected) {
|
|
212
|
-
assertEquals(PdfErrorCode.CANCELLED, expected.code());
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
RasterPixels px = r.render(request, null);
|
|
216
|
-
assertTrue(
|
|
217
|
-
"the render after a cancelled one must be correct",
|
|
218
|
-
PdfCanvasRasterizerTest.isRed(PdfCanvasRasterizerTest.rgba(px, 50, 75)));
|
|
219
|
-
|
|
220
|
-
RasterPixels withFreshSignal = r.render(request, new RasterCancellation());
|
|
221
|
-
assertNotNull("a fresh, uncancelled signal must not refuse", withFreshSignal);
|
|
222
|
-
} finally {
|
|
223
|
-
r.close();
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
package tools.reekon.pdfcanvas;
|
|
2
|
-
|
|
3
|
-
import static org.junit.Assert.assertEquals;
|
|
4
|
-
import static org.junit.Assert.assertNull;
|
|
5
|
-
import static org.junit.Assert.assertTrue;
|
|
6
|
-
|
|
7
|
-
import android.content.Context;
|
|
8
|
-
import android.util.Log;
|
|
9
|
-
|
|
10
|
-
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
11
|
-
import androidx.test.platform.app.InstrumentationRegistry;
|
|
12
|
-
|
|
13
|
-
import org.junit.Test;
|
|
14
|
-
import org.junit.runner.RunWith;
|
|
15
|
-
|
|
16
|
-
import java.io.File;
|
|
17
|
-
import java.util.ArrayList;
|
|
18
|
-
import java.util.List;
|
|
19
|
-
import java.util.concurrent.CountDownLatch;
|
|
20
|
-
import java.util.concurrent.ExecutorService;
|
|
21
|
-
import java.util.concurrent.Executors;
|
|
22
|
-
import java.util.concurrent.TimeUnit;
|
|
23
|
-
import java.util.concurrent.atomic.AtomicReference;
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* The pool, under concurrency.
|
|
27
|
-
*
|
|
28
|
-
* The rule being defended is narrow and absolute: {@code PdfRenderer} allows ONE
|
|
29
|
-
* open page per instance, so two threads on ONE renderer is what does not work.
|
|
30
|
-
* Two renderers is fine. Every test here exists because the failure mode of
|
|
31
|
-
* getting that wrong is either an {@code IllegalStateException} from a thread
|
|
32
|
-
* nobody is watching, or — worse — two renders interleaving into each other's
|
|
33
|
-
* bitmaps and producing two plausible, wrong images.
|
|
34
|
-
*/
|
|
35
|
-
@RunWith(AndroidJUnit4.class)
|
|
36
|
-
public class PdfCanvasConcurrencyTest {
|
|
37
|
-
|
|
38
|
-
private static final String TAG = "PDFCANVAS";
|
|
39
|
-
|
|
40
|
-
private static Context ctx() {
|
|
41
|
-
return InstrumentationRegistry.getInstrumentation().getTargetContext();
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** Eight distinct interior tiles of an ARCH-D sheet, each visually different. */
|
|
45
|
-
private static List<RasterRequest> tileSet() {
|
|
46
|
-
List<RasterRequest> out = new ArrayList<>();
|
|
47
|
-
for (int i = 0; i < 8; i++) {
|
|
48
|
-
float x = 100f + i * 137f;
|
|
49
|
-
float y = 200f + i * 211f;
|
|
50
|
-
out.add(new RasterRequest(0, x, y, 96f, 96f, 3f, false, RasterRequest.BACKGROUND_WHITE));
|
|
51
|
-
}
|
|
52
|
-
return out;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* CATCHES: any interleaving that corrupts a raster once several renders are
|
|
57
|
-
* genuinely in flight through a pool of four.
|
|
58
|
-
*
|
|
59
|
-
* References are computed SERIALLY first, on one thread, so they are known
|
|
60
|
-
* good. Every concurrent result is then compared byte-for-byte against its
|
|
61
|
-
* reference — because the dangerous failure is not the one that throws, it is
|
|
62
|
-
* two renders mixing pixels and both coming back plausible.
|
|
63
|
-
*
|
|
64
|
-
* WHAT THIS TEST DOES NOT RELIABLY CATCH, corrected after measuring rather
|
|
65
|
-
* than assumed. It used to claim it catches "a pool that hands the same
|
|
66
|
-
* PdfRenderer to two threads". It does not, or not dependably: with
|
|
67
|
-
* {@code lease()} mutated to hand every thread renderer #0, this test failed
|
|
68
|
-
* on API 24 and 33 in one run and on API 24 and 31 in the next — the threads
|
|
69
|
-
* simply do not always collide inside the {@code openPage}..{@code close}
|
|
70
|
-
* window, which for these 288x288 tiles is a fraction of a millisecond. A
|
|
71
|
-
* test that catches a bug in two runs out of three is a coin toss reported as
|
|
72
|
-
* evidence.
|
|
73
|
-
*
|
|
74
|
-
* The double-lease property is pinned DETERMINISTICALLY by
|
|
75
|
-
* {@link #poolOfOneSerialisesRatherThanDoubleLeasing}, which failed on all
|
|
76
|
-
* three levels in both runs of that same mutation. That is the test to look at
|
|
77
|
-
* if this one ever starts passing while the pool is broken.
|
|
78
|
-
*/
|
|
79
|
-
@Test
|
|
80
|
-
public void concurrentRendersThroughThePoolAreAllCorrect() throws Exception {
|
|
81
|
-
File pdf = TestPdfs.pattern(ctx(), "conc-archd.pdf", TestPdfs.ARCH_D_W, TestPdfs.ARCH_D_H);
|
|
82
|
-
List<RasterRequest> requests = tileSet();
|
|
83
|
-
|
|
84
|
-
PdfCanvasRasterizer serial = PdfCanvasRasterizer.openFile(pdf, null, 1);
|
|
85
|
-
List<RasterPixels> reference = new ArrayList<>();
|
|
86
|
-
long serialStart = System.nanoTime();
|
|
87
|
-
try {
|
|
88
|
-
for (RasterRequest request : requests) {
|
|
89
|
-
reference.add(serial.render(request, null));
|
|
90
|
-
}
|
|
91
|
-
} finally {
|
|
92
|
-
serial.close();
|
|
93
|
-
}
|
|
94
|
-
long serialMs = (System.nanoTime() - serialStart) / 1_000_000;
|
|
95
|
-
|
|
96
|
-
PdfCanvasRasterizer parallel = PdfCanvasRasterizer.openFile(pdf, null, 4);
|
|
97
|
-
assertEquals("pool should have been created at the requested size", 4, parallel.poolSize());
|
|
98
|
-
|
|
99
|
-
int threads = 4;
|
|
100
|
-
int rounds = 3;
|
|
101
|
-
ExecutorService pool = Executors.newFixedThreadPool(threads);
|
|
102
|
-
CountDownLatch go = new CountDownLatch(1);
|
|
103
|
-
CountDownLatch done = new CountDownLatch(threads);
|
|
104
|
-
AtomicReference<Throwable> failure = new AtomicReference<>();
|
|
105
|
-
|
|
106
|
-
long parallelStart;
|
|
107
|
-
try {
|
|
108
|
-
for (int t = 0; t < threads; t++) {
|
|
109
|
-
final int threadIndex = t;
|
|
110
|
-
pool.execute(
|
|
111
|
-
() -> {
|
|
112
|
-
try {
|
|
113
|
-
go.await();
|
|
114
|
-
for (int round = 0; round < rounds; round++) {
|
|
115
|
-
for (int i = threadIndex; i < requests.size(); i += threads) {
|
|
116
|
-
RasterPixels got = parallel.render(requests.get(i), null);
|
|
117
|
-
RasterPixels want = reference.get(i);
|
|
118
|
-
assertEquals("width for tile " + i, want.width, got.width);
|
|
119
|
-
PdfCanvasRasterizerTest.Diff diff =
|
|
120
|
-
PdfCanvasRasterizerTest.compareRegion(got, want, 0, 0);
|
|
121
|
-
if (diff.mismatches != 0) {
|
|
122
|
-
throw new AssertionError("tile " + i + " differs from its serial "
|
|
123
|
-
+ "reference: " + diff);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
} catch (Throwable e) {
|
|
128
|
-
failure.compareAndSet(null, e);
|
|
129
|
-
} finally {
|
|
130
|
-
done.countDown();
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
parallelStart = System.nanoTime();
|
|
135
|
-
go.countDown();
|
|
136
|
-
assertTrue("threads did not finish in 120s", done.await(120, TimeUnit.SECONDS));
|
|
137
|
-
} finally {
|
|
138
|
-
pool.shutdownNow();
|
|
139
|
-
parallel.close();
|
|
140
|
-
}
|
|
141
|
-
long parallelMs = (System.nanoTime() - parallelStart) / 1_000_000;
|
|
142
|
-
|
|
143
|
-
assertNull(
|
|
144
|
-
failure.get() == null ? null : String.valueOf(failure.get()), failure.get());
|
|
145
|
-
|
|
146
|
-
// Reported, not asserted. AOSP serialises every PdfRenderer native call
|
|
147
|
-
// behind a process-wide PDFium lock, so the pool buys CORRECTNESS (no shared
|
|
148
|
-
// open page) rather than throughput. If these two numbers are close, that is
|
|
149
|
-
// the lock; if the parallel run is meaningfully faster, this platform build
|
|
150
|
-
// does not have it. Either way the pool is still required.
|
|
151
|
-
Log.i(TAG, "concurrency: " + requests.size() + " tiles serial=" + serialMs + "ms | "
|
|
152
|
-
+ (threads * rounds * requests.size() / threads) + " tiles across " + threads
|
|
153
|
-
+ " threads, pool=4, =" + parallelMs + "ms");
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* CATCHES: a pool that leases the same renderer twice under contention. This
|
|
158
|
-
* is THE test for that property, and the only one that catches it every time.
|
|
159
|
-
*
|
|
160
|
-
* With a pool of ONE and four threads pushing, every request must queue. A
|
|
161
|
-
* correct pool serialises them and every result is right; a broken lease hands
|
|
162
|
-
* renderer #0 to two threads at once, and {@code openPage} throws
|
|
163
|
-
* "Current page not closed" — which this backend reports as a
|
|
164
|
-
* {@code backend-failure} naming the double lease, so the failure message
|
|
165
|
-
* points straight at the cause.
|
|
166
|
-
*
|
|
167
|
-
* Why this one is deterministic where
|
|
168
|
-
* {@link #concurrentRendersThroughThePoolAreAllCorrect} is not: a pool of one
|
|
169
|
-
* means EVERY pair of threads contends for the same renderer, so a lease that
|
|
170
|
-
* does not remove the renderer from the free queue collides on essentially the
|
|
171
|
-
* first overlap rather than on whichever pair of the four happens to line up.
|
|
172
|
-
* Measured: mutating {@code lease()} to hand out renderer #0 without taking it
|
|
173
|
-
* failed here on API 24, 31 and 33 in both runs, while the four-renderer test
|
|
174
|
-
* caught it on only two of the three each time.
|
|
175
|
-
*/
|
|
176
|
-
@Test
|
|
177
|
-
public void poolOfOneSerialisesRatherThanDoubleLeasing() throws Exception {
|
|
178
|
-
File pdf = TestPdfs.pattern(ctx(), "conc1-letter.pdf", TestPdfs.LETTER_W, TestPdfs.LETTER_H);
|
|
179
|
-
RasterRequest request =
|
|
180
|
-
new RasterRequest(0, 100f, 200f, 200f, 200f, 2f, false, RasterRequest.BACKGROUND_WHITE);
|
|
181
|
-
|
|
182
|
-
PdfCanvasRasterizer r = PdfCanvasRasterizer.openFile(pdf, null, 1);
|
|
183
|
-
assertEquals(1, r.poolSize());
|
|
184
|
-
RasterPixels reference = r.render(request, null);
|
|
185
|
-
|
|
186
|
-
int threads = 4;
|
|
187
|
-
ExecutorService pool = Executors.newFixedThreadPool(threads);
|
|
188
|
-
CountDownLatch go = new CountDownLatch(1);
|
|
189
|
-
CountDownLatch done = new CountDownLatch(threads);
|
|
190
|
-
AtomicReference<Throwable> failure = new AtomicReference<>();
|
|
191
|
-
try {
|
|
192
|
-
for (int t = 0; t < threads; t++) {
|
|
193
|
-
pool.execute(
|
|
194
|
-
() -> {
|
|
195
|
-
try {
|
|
196
|
-
go.await();
|
|
197
|
-
for (int i = 0; i < 6; i++) {
|
|
198
|
-
RasterPixels got = r.render(request, null);
|
|
199
|
-
PdfCanvasRasterizerTest.Diff diff =
|
|
200
|
-
PdfCanvasRasterizerTest.compareRegion(got, reference, 0, 0);
|
|
201
|
-
if (diff.mismatches != 0) {
|
|
202
|
-
throw new AssertionError("serialised render differs: " + diff);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
} catch (Throwable e) {
|
|
206
|
-
failure.compareAndSet(null, e);
|
|
207
|
-
} finally {
|
|
208
|
-
done.countDown();
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
go.countDown();
|
|
213
|
-
assertTrue("threads did not finish in 120s", done.await(120, TimeUnit.SECONDS));
|
|
214
|
-
} finally {
|
|
215
|
-
pool.shutdownNow();
|
|
216
|
-
r.close();
|
|
217
|
-
}
|
|
218
|
-
assertNull(String.valueOf(failure.get()), failure.get());
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* CATCHES: a pool that clamps its size wrongly, or accepts a nonsense size and
|
|
223
|
-
* then allocates that many renderers. Each renderer holds an fd and a parsed
|
|
224
|
-
* document, so an unclamped 64 would be 64 of both.
|
|
225
|
-
*/
|
|
226
|
-
@Test
|
|
227
|
-
public void poolSizeIsClamped() throws Exception {
|
|
228
|
-
File pdf = TestPdfs.pattern(ctx(), "clamp.pdf", TestPdfs.LETTER_W, TestPdfs.LETTER_H);
|
|
229
|
-
PdfCanvasRasterizer zero = PdfCanvasRasterizer.openFile(pdf, null, 0);
|
|
230
|
-
PdfCanvasRasterizer huge = PdfCanvasRasterizer.openFile(pdf, null, 64);
|
|
231
|
-
try {
|
|
232
|
-
assertEquals(1, zero.poolSize());
|
|
233
|
-
assertEquals(PdfRendererPool.MAX_POOL_SIZE, huge.poolSize());
|
|
234
|
-
} finally {
|
|
235
|
-
zero.close();
|
|
236
|
-
huge.close();
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|