@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,783 +0,0 @@
|
|
|
1
|
-
package tools.reekon.pdfcanvas;
|
|
2
|
-
|
|
3
|
-
import static org.junit.Assert.assertArrayEquals;
|
|
4
|
-
import static org.junit.Assert.assertEquals;
|
|
5
|
-
import static org.junit.Assert.assertFalse;
|
|
6
|
-
import static org.junit.Assert.assertNotNull;
|
|
7
|
-
import static org.junit.Assert.assertNull;
|
|
8
|
-
import static org.junit.Assert.assertTrue;
|
|
9
|
-
|
|
10
|
-
import android.content.Context;
|
|
11
|
-
import android.util.Base64;
|
|
12
|
-
import android.util.Log;
|
|
13
|
-
|
|
14
|
-
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
15
|
-
import androidx.test.platform.app.InstrumentationRegistry;
|
|
16
|
-
|
|
17
|
-
import org.junit.Before;
|
|
18
|
-
import org.junit.Test;
|
|
19
|
-
import org.junit.runner.RunWith;
|
|
20
|
-
|
|
21
|
-
import java.io.File;
|
|
22
|
-
import java.util.ArrayList;
|
|
23
|
-
import java.util.List;
|
|
24
|
-
import java.util.concurrent.Callable;
|
|
25
|
-
import java.util.concurrent.CountDownLatch;
|
|
26
|
-
import java.util.concurrent.ExecutorService;
|
|
27
|
-
import java.util.concurrent.Executors;
|
|
28
|
-
import java.util.concurrent.Future;
|
|
29
|
-
import java.util.concurrent.TimeUnit;
|
|
30
|
-
import java.util.concurrent.atomic.AtomicReference;
|
|
31
|
-
|
|
32
|
-
import tools.reekon.pdfcanvas.jsi.PdfCanvasPixelBridge;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* The raster transport's byte fidelity, on device.
|
|
36
|
-
*
|
|
37
|
-
* <h3>What this can and cannot reach</h3>
|
|
38
|
-
*
|
|
39
|
-
* This is the STANDALONE build: no React Native, no Hermes, no libjsi.so — see
|
|
40
|
-
* {@code android/build.gradle}. So the JS half of the transport is out of reach
|
|
41
|
-
* here and always will be. What IS in reach, and is the only part that can
|
|
42
|
-
* corrupt a raster, is the JNI copy: {@code nativeStore} reads a Java
|
|
43
|
-
* {@code byte[]} into the C++ buffer that the ArrayBuffer will later wrap
|
|
44
|
-
* WITHOUT COPYING. Everything downstream of that buffer is a pointer hand-off;
|
|
45
|
-
* everything upstream is the rasterizer, which the rest of this suite proves.
|
|
46
|
-
*
|
|
47
|
-
* So the question worth asking on device is exactly one: are the bytes that
|
|
48
|
-
* enter the transport the same bytes the base64 path would have delivered? A
|
|
49
|
-
* transport that is fast and wrong is worthless, and it would fail silently —
|
|
50
|
-
* a stride slip shears, a channel swap discolours, a signed-byte widening
|
|
51
|
-
* mangles every value above 0x7F. None of those throw.
|
|
52
|
-
*
|
|
53
|
-
* <h3>TWO fills now, and the second one is the shipped one</h3>
|
|
54
|
-
*
|
|
55
|
-
* {@code nativeStore} reads a Java {@code byte[]}. {@code nativeStoreFromBitmap}
|
|
56
|
-
* reads the {@code Bitmap}'s own pixels through
|
|
57
|
-
* {@code AndroidBitmap_lockPixels} and is what a production render actually
|
|
58
|
-
* uses — it deletes the {@code byte[]} entirely. They are different memcpys
|
|
59
|
-
* from different sources, so proving one says nothing about the other, and
|
|
60
|
-
* section 5 asks the same fidelity question of the second.
|
|
61
|
-
*
|
|
62
|
-
* <p>That section is also the ONLY place the direct fill can be checked at all.
|
|
63
|
-
* The install-time probe that gates the whole transport
|
|
64
|
-
* ({@code PdfCanvasModule.installJsiTransport}) round-trips
|
|
65
|
-
* {@link PdfCanvasPixelBridge#PROBE_BYTES} through {@code nativeStore}, because
|
|
66
|
-
* a probe needs bytes both sides already agree on and there is no Bitmap at
|
|
67
|
-
* install time. What the probe proves is the JS half — the host function is
|
|
68
|
-
* installed, Hermes will wrap a {@code MutableBuffer}, byte values survive the
|
|
69
|
-
* ArrayBuffer view. It does NOT exercise the fill that every subsequent raster
|
|
70
|
-
* uses. This file is what covers that, which is why it ships in the standalone
|
|
71
|
-
* build rather than living behind an app.
|
|
72
|
-
*
|
|
73
|
-
* <h3>Why the comparison is against base64 rather than against the bitmap</h3>
|
|
74
|
-
*
|
|
75
|
-
* Because base64 is the thing being replaced. {@code Base64.encodeToString} then
|
|
76
|
-
* {@code Base64.decode} is the round trip the shipped transport performs, so
|
|
77
|
-
* asserting the new path equals it is asserting the replacement is a
|
|
78
|
-
* replacement. Comparing against {@code pixels.bytes} directly would only prove
|
|
79
|
-
* the copy copied — true of a path that had never been through base64 either.
|
|
80
|
-
*/
|
|
81
|
-
@RunWith(AndroidJUnit4.class)
|
|
82
|
-
public class PdfCanvasJsiTransportTest {
|
|
83
|
-
|
|
84
|
-
private static final String TAG = "PDFCANVAS";
|
|
85
|
-
|
|
86
|
-
private static Context ctx() {
|
|
87
|
-
return InstrumentationRegistry.getInstrumentation().getTargetContext();
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** Slot ids are the caller's to choose; this suite owns a range of its own. */
|
|
91
|
-
private static long slot = 1_000_000L;
|
|
92
|
-
|
|
93
|
-
private static long nextSlot() {
|
|
94
|
-
return slot++;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
@Before
|
|
98
|
-
public void drainSlots() {
|
|
99
|
-
// Each test starts from a known slot count, so the leak assertions below
|
|
100
|
-
// measure this test rather than the one before it.
|
|
101
|
-
PdfCanvasPixelBridge.nativeReleaseAll();
|
|
102
|
-
assertEquals(0, PdfCanvasPixelBridge.nativeSlotCount());
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/* ================================================================ *
|
|
106
|
-
* 1. The library loaded at all
|
|
107
|
-
* ================================================================ */
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* CATCHES: the native build silently not running.
|
|
111
|
-
*
|
|
112
|
-
* {@code PdfCanvasPixelBridge.isLoaded()} is false-tolerant by design — every
|
|
113
|
-
* caller falls back to base64 — which means a CMake misconfiguration, a
|
|
114
|
-
* missing ABI or a dropped source set produces a working app that is simply
|
|
115
|
-
* never fast. There is no error anywhere. This is the assertion that turns
|
|
116
|
-
* that into a red test.
|
|
117
|
-
*/
|
|
118
|
-
@Test
|
|
119
|
-
public void nativeTransportLibraryIsPresent() {
|
|
120
|
-
assertTrue(
|
|
121
|
-
"libpdfcanvasjsi.so did not load. Every render will fall back to base64.",
|
|
122
|
-
PdfCanvasPixelBridge.isLoaded());
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* The other half of the two-build split, asserted rather than assumed.
|
|
127
|
-
*
|
|
128
|
-
* This build compiles the transport with {@code PDFCANVAS_WITH_JSI} undefined,
|
|
129
|
-
* so there is no {@code jsi/jsi.h} and no host function to install. Install
|
|
130
|
-
* must report false rather than crash — a JNI method that is declared but has
|
|
131
|
-
* no definition throws {@code UnsatisfiedLinkError} at CALL time, and that is
|
|
132
|
-
* exactly the shape of failure this asserts is absent.
|
|
133
|
-
*/
|
|
134
|
-
@Test
|
|
135
|
-
public void installReportsFalseInABuildWithoutJsi() {
|
|
136
|
-
assertFalse(PdfCanvasPixelBridge.nativeInstall(0L));
|
|
137
|
-
// A non-null pointer must be refused just as calmly: there is no runtime
|
|
138
|
-
// here for it to point at.
|
|
139
|
-
assertFalse(PdfCanvasPixelBridge.nativeInstall(1L));
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/* ================================================================ *
|
|
143
|
-
* 2. Byte fidelity against the transport being replaced
|
|
144
|
-
* ================================================================ */
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* THE TEST THIS FILE EXISTS FOR.
|
|
148
|
-
*
|
|
149
|
-
* A real raster of a four-colour page, moved through the transport, must be
|
|
150
|
-
* byte-identical to the same raster moved through base64.
|
|
151
|
-
*
|
|
152
|
-
* CATCHES: a channel swap (R and B differ per quadrant), a stride slip (the
|
|
153
|
-
* quadrant boundary would drift one pixel per row), a truncated tail (the last
|
|
154
|
-
* row is BLACK here and its bytes are 0x00/0xFF, so a short copy shows up as a
|
|
155
|
-
* length mismatch rather than as plausible content), and a signed-byte
|
|
156
|
-
* widening — {@code Color.RED} is 0xFF in one channel, above Java's
|
|
157
|
-
* {@code byte} range, and {@code GetByteArrayRegion} is the one call in this
|
|
158
|
-
* path that could get it wrong.
|
|
159
|
-
*/
|
|
160
|
-
@Test
|
|
161
|
-
public void transportBytesEqualBase64BytesForARealRaster() throws Exception {
|
|
162
|
-
File pdf = TestPdfs.quadrants(ctx(), "jsi-quadrants.pdf", TestPdfs.LETTER_W, TestPdfs.LETTER_H);
|
|
163
|
-
PdfCanvasRasterizer r = PdfCanvasRasterizer.openFile(pdf, null, 1);
|
|
164
|
-
try {
|
|
165
|
-
RasterPixels pixels =
|
|
166
|
-
r.render(
|
|
167
|
-
new RasterRequest(
|
|
168
|
-
0, 0f, 0f, TestPdfs.LETTER_W, TestPdfs.LETTER_H, 2f, false,
|
|
169
|
-
RasterRequest.BACKGROUND_WHITE),
|
|
170
|
-
null);
|
|
171
|
-
|
|
172
|
-
// What the shipped transport delivers to JS, produced by the shipped calls.
|
|
173
|
-
byte[] viaBase64 =
|
|
174
|
-
Base64.decode(Base64.encodeToString(pixels.bytes, Base64.NO_WRAP), Base64.DEFAULT);
|
|
175
|
-
|
|
176
|
-
long id = nextSlot();
|
|
177
|
-
PdfCanvasPixelBridge.nativeStore(id, pixels.bytes);
|
|
178
|
-
byte[] viaTransport = PdfCanvasPixelBridge.nativeTakeForTest(id);
|
|
179
|
-
|
|
180
|
-
assertNotNull("slot came back empty", viaTransport);
|
|
181
|
-
assertEquals(
|
|
182
|
-
"length differs: " + viaBase64.length + " vs " + viaTransport.length,
|
|
183
|
-
viaBase64.length,
|
|
184
|
-
viaTransport.length);
|
|
185
|
-
assertArrayEquals(viaBase64, viaTransport);
|
|
186
|
-
|
|
187
|
-
// And the reference is itself right — otherwise the assertion above is two
|
|
188
|
-
// wrong things agreeing. Top-left is RED in an RGBA_8888 buffer.
|
|
189
|
-
assertEquals(255, pixels.channel(4, 4, 0));
|
|
190
|
-
assertEquals(0, pixels.channel(4, 4, 1));
|
|
191
|
-
assertEquals(0, pixels.channel(4, 4, 2));
|
|
192
|
-
|
|
193
|
-
Log.i(
|
|
194
|
-
TAG,
|
|
195
|
-
"PDFCANVAS-TRANSPORT identical bytes="
|
|
196
|
-
+ viaTransport.length
|
|
197
|
-
+ " ("
|
|
198
|
-
+ pixels.width
|
|
199
|
-
+ "x"
|
|
200
|
-
+ pixels.height
|
|
201
|
-
+ " rowBytes="
|
|
202
|
-
+ pixels.rowBytes
|
|
203
|
-
+ ")");
|
|
204
|
-
} finally {
|
|
205
|
-
r.close();
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* The same claim at the FIELD'S tile size, because a 4 MiB copy is a different
|
|
211
|
-
* call from a 40 KiB one.
|
|
212
|
-
*
|
|
213
|
-
* CATCHES: an {@code int} overflow or a truncating cast in the JNI length
|
|
214
|
-
* handling, and any per-chunk copy that loses a boundary. 1024x1024 at 131.25
|
|
215
|
-
* devpx/pt is the exact geometry the split measurement used, so this is also
|
|
216
|
-
* the payload the speed numbers are quoted against.
|
|
217
|
-
*/
|
|
218
|
-
@Test
|
|
219
|
-
public void transportBytesEqualBase64BytesForAFieldSizedTile() throws Exception {
|
|
220
|
-
File pdf = TestPdfs.pattern(ctx(), "jsi-tile.pdf", TestPdfs.LETTER_W, TestPdfs.LETTER_H);
|
|
221
|
-
PdfCanvasRasterizer r = PdfCanvasRasterizer.openFile(pdf, null, 1);
|
|
222
|
-
try {
|
|
223
|
-
final float scale = 131.25f;
|
|
224
|
-
final float points = 1024f / scale;
|
|
225
|
-
RasterPixels pixels =
|
|
226
|
-
r.render(
|
|
227
|
-
new RasterRequest(0, 0f, 0f, points, points, scale, false,
|
|
228
|
-
RasterRequest.BACKGROUND_WHITE),
|
|
229
|
-
null);
|
|
230
|
-
assertEquals(1024, pixels.width);
|
|
231
|
-
assertEquals(1024, pixels.height);
|
|
232
|
-
assertEquals(1024 * 1024 * 4, pixels.bytes.length);
|
|
233
|
-
|
|
234
|
-
byte[] viaBase64 =
|
|
235
|
-
Base64.decode(Base64.encodeToString(pixels.bytes, Base64.NO_WRAP), Base64.DEFAULT);
|
|
236
|
-
|
|
237
|
-
long id = nextSlot();
|
|
238
|
-
PdfCanvasPixelBridge.nativeStore(id, pixels.bytes);
|
|
239
|
-
byte[] viaTransport = PdfCanvasPixelBridge.nativeTakeForTest(id);
|
|
240
|
-
|
|
241
|
-
assertNotNull(viaTransport);
|
|
242
|
-
assertArrayEquals(viaBase64, viaTransport);
|
|
243
|
-
} finally {
|
|
244
|
-
r.close();
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Every byte value, so the sign question is settled outright rather than by
|
|
250
|
-
* whichever values a rasterized page happened to contain.
|
|
251
|
-
*
|
|
252
|
-
* CATCHES: {@code GetByteArrayRegion} into a {@code uint8_t*} going through a
|
|
253
|
-
* sign-extending conversion. Values 0x80-0xFF are the entire failure set and
|
|
254
|
-
* a white-background raster is mostly 0xFF, which would HIDE the bug rather
|
|
255
|
-
* than expose it — a page that is all 0xFF looks identical whether or not the
|
|
256
|
-
* copy is correct for 0x80.
|
|
257
|
-
*/
|
|
258
|
-
@Test
|
|
259
|
-
public void everyByteValueSurvivesTheCopy() {
|
|
260
|
-
byte[] all = new byte[256 * 7];
|
|
261
|
-
for (int i = 0; i < all.length; i++) {
|
|
262
|
-
all[i] = (byte) (i & 0xFF);
|
|
263
|
-
}
|
|
264
|
-
long id = nextSlot();
|
|
265
|
-
PdfCanvasPixelBridge.nativeStore(id, all);
|
|
266
|
-
assertArrayEquals(all, PdfCanvasPixelBridge.nativeTakeForTest(id));
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* The probe payload the JS negotiation compares against, round-tripped here.
|
|
271
|
-
*
|
|
272
|
-
* CATCHES: the two restatements of the probe drifting apart. Java's copy is
|
|
273
|
-
* {@code PdfCanvasPixelBridge.PROBE_BYTES} and TypeScript's is
|
|
274
|
-
* {@code TRANSPORT_PROBE_BYTES} in {@code src/rasterizer/android.ts}; they are
|
|
275
|
-
* restated rather than derived, on purpose, so that a divergence disables the
|
|
276
|
-
* fast path instead of shipping a shear. This asserts the Java half at least
|
|
277
|
-
* survives its own transport — the values are checked literally so that
|
|
278
|
-
* editing the constant without editing the TypeScript is a red test HERE,
|
|
279
|
-
* where the reason is written down, rather than a silent fallback in the field.
|
|
280
|
-
*/
|
|
281
|
-
@Test
|
|
282
|
-
public void probeBytesRoundTrip() {
|
|
283
|
-
assertArrayEquals(
|
|
284
|
-
new byte[] {0x00, 0x01, 0x02, (byte) 0xFD, (byte) 0xFE, (byte) 0xFF, 0x2A, 0x07},
|
|
285
|
-
PdfCanvasPixelBridge.PROBE_BYTES);
|
|
286
|
-
|
|
287
|
-
long id = nextSlot();
|
|
288
|
-
PdfCanvasPixelBridge.nativeStore(id, PdfCanvasPixelBridge.PROBE_BYTES);
|
|
289
|
-
assertArrayEquals(
|
|
290
|
-
PdfCanvasPixelBridge.PROBE_BYTES, PdfCanvasPixelBridge.nativeTakeForTest(id));
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/* ================================================================ *
|
|
294
|
-
* 3. Slot lifecycle — where a raster-sized leak would live
|
|
295
|
-
* ================================================================ */
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* CATCHES: a slot that survives being taken.
|
|
299
|
-
*
|
|
300
|
-
* The ArrayBuffer handed to JS owns the buffer from the moment the take
|
|
301
|
-
* succeeds. If the map kept a reference too, every raster would stay resident
|
|
302
|
-
* for the life of the process — tens of megabytes per epoch — and nothing
|
|
303
|
-
* would report it.
|
|
304
|
-
*/
|
|
305
|
-
@Test
|
|
306
|
-
public void takingASlotEmptiesIt() {
|
|
307
|
-
long id = nextSlot();
|
|
308
|
-
PdfCanvasPixelBridge.nativeStore(id, new byte[] {1, 2, 3, 4});
|
|
309
|
-
assertEquals(1, PdfCanvasPixelBridge.nativeSlotCount());
|
|
310
|
-
|
|
311
|
-
assertArrayEquals(new byte[] {1, 2, 3, 4}, PdfCanvasPixelBridge.nativeTakeForTest(id));
|
|
312
|
-
assertEquals(0, PdfCanvasPixelBridge.nativeSlotCount());
|
|
313
|
-
|
|
314
|
-
// Second take is empty rather than a stale buffer. On the JS side this is
|
|
315
|
-
// what becomes a named throw instead of a black tile.
|
|
316
|
-
assertNull(PdfCanvasPixelBridge.nativeTakeForTest(id));
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/** CATCHES: release doing nothing, so a cancelled raster is held forever. */
|
|
320
|
-
@Test
|
|
321
|
-
public void releaseDropsASlotWithoutReadingIt() {
|
|
322
|
-
long id = nextSlot();
|
|
323
|
-
PdfCanvasPixelBridge.nativeStore(id, new byte[1024]);
|
|
324
|
-
PdfCanvasPixelBridge.nativeRelease(id);
|
|
325
|
-
assertEquals(0, PdfCanvasPixelBridge.nativeSlotCount());
|
|
326
|
-
assertNull(PdfCanvasPixelBridge.nativeTakeForTest(id));
|
|
327
|
-
|
|
328
|
-
// Releasing an absent slot is a no-op, not a crash: `invalidate()` runs it
|
|
329
|
-
// over slots that may already have been taken.
|
|
330
|
-
PdfCanvasPixelBridge.nativeRelease(id);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* CATCHES: {@code invalidate()} leaving stranded rasters behind.
|
|
335
|
-
*
|
|
336
|
-
* A Fast Refresh drops the JS side between a promise resolving and its
|
|
337
|
-
* continuation running, so slots stored for those renders can never be taken.
|
|
338
|
-
* Dozens of reloads a day at several megabytes each is the whole budget.
|
|
339
|
-
*/
|
|
340
|
-
@Test
|
|
341
|
-
public void releaseAllDrainsEverything() {
|
|
342
|
-
for (int i = 0; i < 16; i++) {
|
|
343
|
-
PdfCanvasPixelBridge.nativeStore(nextSlot(), new byte[64 * 1024]);
|
|
344
|
-
}
|
|
345
|
-
assertEquals(16, PdfCanvasPixelBridge.nativeSlotCount());
|
|
346
|
-
PdfCanvasPixelBridge.nativeReleaseAll();
|
|
347
|
-
assertEquals(0, PdfCanvasPixelBridge.nativeSlotCount());
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/* ================================================================ *
|
|
351
|
-
* 4. Threading
|
|
352
|
-
* ================================================================ */
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* CATCHES: an unsynchronised slot map.
|
|
356
|
-
*
|
|
357
|
-
* In production the stores happen on the render executor — up to
|
|
358
|
-
* {@code MAX_POOL_SIZE} threads at once, one per lane — while the takes happen
|
|
359
|
-
* on the JS thread. An unlocked {@code std::unordered_map} under that pattern
|
|
360
|
-
* does not usually throw; it corrupts a bucket chain and either loses a slot
|
|
361
|
-
* or returns the wrong one, which on the JS side is a tile drawn with another
|
|
362
|
-
* tile's pixels. Four writers and a concurrent reader for 200 rounds is enough
|
|
363
|
-
* to make an unguarded map fail reliably.
|
|
364
|
-
*/
|
|
365
|
-
@Test
|
|
366
|
-
public void concurrentStoresAndTakesKeepTheirOwnBytes() throws Exception {
|
|
367
|
-
final int threads = 4;
|
|
368
|
-
final int rounds = 200;
|
|
369
|
-
ExecutorService pool = Executors.newFixedThreadPool(threads);
|
|
370
|
-
try {
|
|
371
|
-
List<Future<Boolean>> futures = new ArrayList<>();
|
|
372
|
-
for (int t = 0; t < threads; t++) {
|
|
373
|
-
final int lane = t;
|
|
374
|
-
futures.add(
|
|
375
|
-
pool.submit(
|
|
376
|
-
(Callable<Boolean>)
|
|
377
|
-
() -> {
|
|
378
|
-
for (int i = 0; i < rounds; i++) {
|
|
379
|
-
byte[] payload = new byte[512];
|
|
380
|
-
// Lane-stamped, so a slot returned to the wrong caller is
|
|
381
|
-
// detectable rather than merely "some bytes".
|
|
382
|
-
for (int b = 0; b < payload.length; b++) {
|
|
383
|
-
payload[b] = (byte) ((lane * 37 + i * 11 + b) & 0xFF);
|
|
384
|
-
}
|
|
385
|
-
long id;
|
|
386
|
-
synchronized (PdfCanvasJsiTransportTest.class) {
|
|
387
|
-
id = nextSlot();
|
|
388
|
-
}
|
|
389
|
-
PdfCanvasPixelBridge.nativeStore(id, payload);
|
|
390
|
-
byte[] back = PdfCanvasPixelBridge.nativeTakeForTest(id);
|
|
391
|
-
if (back == null || !java.util.Arrays.equals(payload, back)) {
|
|
392
|
-
return false;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
return true;
|
|
396
|
-
}));
|
|
397
|
-
}
|
|
398
|
-
for (Future<Boolean> future : futures) {
|
|
399
|
-
assertTrue("a lane got back bytes it did not store", future.get(60, TimeUnit.SECONDS));
|
|
400
|
-
}
|
|
401
|
-
assertEquals(0, PdfCanvasPixelBridge.nativeSlotCount());
|
|
402
|
-
} finally {
|
|
403
|
-
pool.shutdownNow();
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
/**
|
|
408
|
-
* MEASUREMENT, not verification — the Java-side cost the new transport ADDS.
|
|
409
|
-
*
|
|
410
|
-
* The base64 encode it replaces was 19.88 ms p50 for this payload. This is the
|
|
411
|
-
* number that has to be smaller, and it is the only new cost on the native
|
|
412
|
-
* side. Logged rather than asserted: a threshold here would fail on a slower
|
|
413
|
-
* device for a reason that is not a bug.
|
|
414
|
-
*/
|
|
415
|
-
@Test
|
|
416
|
-
public void measuresTheStoreCostAgainstTheBase64EncodeItReplaces() {
|
|
417
|
-
byte[] tile = new byte[1024 * 1024 * 4];
|
|
418
|
-
for (int i = 0; i < tile.length; i++) tile[i] = (byte) (i & 0xFF);
|
|
419
|
-
|
|
420
|
-
final int iterations = 7;
|
|
421
|
-
long[] store = new long[iterations];
|
|
422
|
-
long[] encode = new long[iterations];
|
|
423
|
-
for (int i = 0; i < iterations; i++) {
|
|
424
|
-
long id = nextSlot();
|
|
425
|
-
long t0 = System.nanoTime();
|
|
426
|
-
PdfCanvasPixelBridge.nativeStore(id, tile);
|
|
427
|
-
store[i] = System.nanoTime() - t0;
|
|
428
|
-
PdfCanvasPixelBridge.nativeRelease(id);
|
|
429
|
-
|
|
430
|
-
long t1 = System.nanoTime();
|
|
431
|
-
String encoded = Base64.encodeToString(tile, Base64.NO_WRAP);
|
|
432
|
-
encode[i] = System.nanoTime() - t1;
|
|
433
|
-
assertTrue(encoded.length() > 0);
|
|
434
|
-
}
|
|
435
|
-
java.util.Arrays.sort(store);
|
|
436
|
-
java.util.Arrays.sort(encode);
|
|
437
|
-
Log.i(
|
|
438
|
-
TAG,
|
|
439
|
-
"PDFCANVAS-TRANSPORT-COST bytes="
|
|
440
|
-
+ tile.length
|
|
441
|
-
+ " storeMs="
|
|
442
|
-
+ (store[iterations / 2] / 1e6)
|
|
443
|
-
+ " base64EncodeMs="
|
|
444
|
-
+ (encode[iterations / 2] / 1e6));
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
/* ================================================================ *
|
|
448
|
-
* 5. The ONE-COPY fill: Bitmap -> native memory, no Java heap
|
|
449
|
-
* ================================================================ */
|
|
450
|
-
|
|
451
|
-
/**
|
|
452
|
-
* THE TEST THE ONE-COPY PATH EXISTS TO BE HELD TO.
|
|
453
|
-
*
|
|
454
|
-
* {@code renderToNativeSlot} never materialises a {@code byte[]}: the raster
|
|
455
|
-
* goes from the {@code Bitmap}'s pixels into the slot in one memcpy. So the
|
|
456
|
-
* only way to know it moved the RIGHT bytes is to render the same request
|
|
457
|
-
* twice — once down the heap path, once down the direct path — and compare.
|
|
458
|
-
*
|
|
459
|
-
* CATCHES everything the heap path is already checked for, because the direct
|
|
460
|
-
* fill re-derives all of it from {@code AndroidBitmap_getInfo} instead of from
|
|
461
|
-
* {@code Bitmap.getRowBytes}: a stride disagreement between the NDK and the
|
|
462
|
-
* Java API (the quadrant boundary would drift one pixel per row), a format
|
|
463
|
-
* mismatch reading ARGB as something else (R and B swap), and a short copy
|
|
464
|
-
* (the two arrays would differ in length). It also catches the fill silently
|
|
465
|
-
* refusing on every device, which would leave the transport correct and
|
|
466
|
-
* permanently slow with nothing anywhere to say so — hence the assert that
|
|
467
|
-
* the raster really did come back in a slot.
|
|
468
|
-
*/
|
|
469
|
-
@Test
|
|
470
|
-
public void directFillBytesEqualTheHeapPathForARealRaster() throws Exception {
|
|
471
|
-
File pdf =
|
|
472
|
-
TestPdfs.quadrants(ctx(), "jsi-direct-quadrants.pdf", TestPdfs.LETTER_W, TestPdfs.LETTER_H);
|
|
473
|
-
PdfCanvasRasterizer r = PdfCanvasRasterizer.openFile(pdf, null, 1);
|
|
474
|
-
try {
|
|
475
|
-
RasterRequest request =
|
|
476
|
-
new RasterRequest(
|
|
477
|
-
0, 0f, 0f, TestPdfs.LETTER_W, TestPdfs.LETTER_H, 2f, false,
|
|
478
|
-
RasterRequest.BACKGROUND_WHITE);
|
|
479
|
-
|
|
480
|
-
RasterPixels viaHeap = r.render(request, null);
|
|
481
|
-
byte[] expected =
|
|
482
|
-
Base64.decode(Base64.encodeToString(viaHeap.bytes, Base64.NO_WRAP), Base64.DEFAULT);
|
|
483
|
-
|
|
484
|
-
long id = nextSlot();
|
|
485
|
-
RasterPixels viaDirect = r.renderToNativeSlot(request, null, id);
|
|
486
|
-
assertTrue(
|
|
487
|
-
"the direct fill refused, so this ran on the heap path and proved nothing "
|
|
488
|
-
+ "about the fill under test",
|
|
489
|
-
viaDirect.isInNativeSlot());
|
|
490
|
-
assertEquals(id, viaDirect.slot);
|
|
491
|
-
assertNull("a slot-backed raster must not also hold a byte[]", viaDirect.bytes);
|
|
492
|
-
// The geometry has to agree too: it is read from the Bitmap on one path
|
|
493
|
-
// and cross-checked against the NDK's view of the same Bitmap on the
|
|
494
|
-
// other, and a disagreement there is what would shear the image.
|
|
495
|
-
assertEquals(viaHeap.width, viaDirect.width);
|
|
496
|
-
assertEquals(viaHeap.height, viaDirect.height);
|
|
497
|
-
assertEquals(viaHeap.rowBytes, viaDirect.rowBytes);
|
|
498
|
-
assertEquals(viaHeap.format, viaDirect.format);
|
|
499
|
-
|
|
500
|
-
byte[] actual = PdfCanvasPixelBridge.nativeTakeForTest(id);
|
|
501
|
-
assertNotNull("the slot came back empty", actual);
|
|
502
|
-
assertEquals(
|
|
503
|
-
"length differs: " + expected.length + " vs " + actual.length,
|
|
504
|
-
expected.length,
|
|
505
|
-
actual.length);
|
|
506
|
-
assertArrayEquals(expected, actual);
|
|
507
|
-
assertEquals(
|
|
508
|
-
"the take must have emptied the slot", 0, PdfCanvasPixelBridge.nativeSlotCount());
|
|
509
|
-
|
|
510
|
-
Log.i(
|
|
511
|
-
TAG,
|
|
512
|
-
"PDFCANVAS-DIRECT identical bytes="
|
|
513
|
-
+ actual.length
|
|
514
|
-
+ " ("
|
|
515
|
-
+ viaDirect.width
|
|
516
|
-
+ "x"
|
|
517
|
-
+ viaDirect.height
|
|
518
|
-
+ " rowBytes="
|
|
519
|
-
+ viaDirect.rowBytes
|
|
520
|
-
+ ")");
|
|
521
|
-
} finally {
|
|
522
|
-
r.close();
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/**
|
|
527
|
-
* The same claim at the FIELD'S tile size, for the same reason the heap path
|
|
528
|
-
* is checked twice: a 4 MiB memcpy is a different call from a 40 KiB one.
|
|
529
|
-
*
|
|
530
|
-
* CATCHES an {@code int} overflow or a truncating cast in the byteCount
|
|
531
|
-
* cross-check — {@code stride * height} is computed in 64-bit on the native
|
|
532
|
-
* side precisely so a 32-bit product cannot wrap into agreement.
|
|
533
|
-
*/
|
|
534
|
-
@Test
|
|
535
|
-
public void directFillBytesEqualTheHeapPathForAFieldSizedTile() throws Exception {
|
|
536
|
-
File pdf =
|
|
537
|
-
TestPdfs.pattern(ctx(), "jsi-direct-tile.pdf", TestPdfs.LETTER_W, TestPdfs.LETTER_H);
|
|
538
|
-
PdfCanvasRasterizer r = PdfCanvasRasterizer.openFile(pdf, null, 1);
|
|
539
|
-
try {
|
|
540
|
-
final float scale = 131.25f;
|
|
541
|
-
final float points = 1024f / scale;
|
|
542
|
-
RasterRequest request =
|
|
543
|
-
new RasterRequest(
|
|
544
|
-
0, 0f, 0f, points, points, scale, false, RasterRequest.BACKGROUND_WHITE);
|
|
545
|
-
|
|
546
|
-
RasterPixels viaHeap = r.render(request, null);
|
|
547
|
-
assertEquals(1024, viaHeap.width);
|
|
548
|
-
assertEquals(1024 * 1024 * 4, viaHeap.bytes.length);
|
|
549
|
-
|
|
550
|
-
long id = nextSlot();
|
|
551
|
-
RasterPixels viaDirect = r.renderToNativeSlot(request, null, id);
|
|
552
|
-
assertTrue("the direct fill refused", viaDirect.isInNativeSlot());
|
|
553
|
-
|
|
554
|
-
byte[] actual = PdfCanvasPixelBridge.nativeTakeForTest(id);
|
|
555
|
-
assertNotNull(actual);
|
|
556
|
-
assertArrayEquals(viaHeap.bytes, actual);
|
|
557
|
-
} finally {
|
|
558
|
-
r.close();
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
/**
|
|
563
|
-
* CATCHES: a slot-backed raster pretending its pixels are readable.
|
|
564
|
-
*
|
|
565
|
-
* {@code RasterPixels.bytes} is null on this path, so every pixel accessor is
|
|
566
|
-
* a latent NPE from somewhere deep in a caller. They throw by name instead,
|
|
567
|
-
* and that has to stay true — the base64 fallback reads {@code bytes}
|
|
568
|
-
* directly and the module branches on {@code isInNativeSlot()} to avoid it.
|
|
569
|
-
*/
|
|
570
|
-
@Test
|
|
571
|
-
public void aDirectFilledRasterRefusesToBeReadFromJava() throws Exception {
|
|
572
|
-
File pdf = TestPdfs.quadrants(ctx(), "jsi-direct-read.pdf", 200, 300);
|
|
573
|
-
PdfCanvasRasterizer r = PdfCanvasRasterizer.openFile(pdf, null, 1);
|
|
574
|
-
try {
|
|
575
|
-
long id = nextSlot();
|
|
576
|
-
RasterPixels px =
|
|
577
|
-
r.renderToNativeSlot(
|
|
578
|
-
new RasterRequest(0, 0f, 0f, 200f, 300f, 1f, false,
|
|
579
|
-
RasterRequest.BACKGROUND_WHITE),
|
|
580
|
-
null,
|
|
581
|
-
id);
|
|
582
|
-
assertTrue(px.isInNativeSlot());
|
|
583
|
-
try {
|
|
584
|
-
px.channel(1, 1, 0);
|
|
585
|
-
throw new AssertionError("channel() read a raster that is not on the Java heap");
|
|
586
|
-
} catch (IllegalStateException expected) {
|
|
587
|
-
assertTrue(
|
|
588
|
-
"the message must name the slot: " + expected.getMessage(),
|
|
589
|
-
expected.getMessage().contains(String.valueOf(id)));
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
// And release() is what the abandon paths call, so it has to actually free.
|
|
593
|
-
assertEquals(1, PdfCanvasPixelBridge.nativeSlotCount());
|
|
594
|
-
px.release();
|
|
595
|
-
assertEquals(0, PdfCanvasPixelBridge.nativeSlotCount());
|
|
596
|
-
// Idempotent: PdfCanvasModule's finally can run after the rasterizer has
|
|
597
|
-
// already released the same raster.
|
|
598
|
-
px.release();
|
|
599
|
-
assertEquals(0, PdfCanvasPixelBridge.nativeSlotCount());
|
|
600
|
-
} finally {
|
|
601
|
-
r.close();
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
/**
|
|
606
|
-
* CATCHES: THE LEAK. A cancel observed AFTER the draw finished must not strand
|
|
607
|
-
* the raster it already filled.
|
|
608
|
-
*
|
|
609
|
-
* This is the one abandon path inside the rasterizer, and it is not
|
|
610
|
-
* hypothetical: a superseded epoch produces a stream of late cancels, so a
|
|
611
|
-
* miss here is megabytes of native memory per gesture, freed only by the next
|
|
612
|
-
* reload. On the heap path the dropped {@code byte[]} was simply collected and
|
|
613
|
-
* there was nothing to get wrong — which is exactly why this needs its own
|
|
614
|
-
* test now.
|
|
615
|
-
*
|
|
616
|
-
* The cancel has to land inside the render window to reach the post-draw
|
|
617
|
-
* check, and the window is device-dependent, so the delay is adapted the same
|
|
618
|
-
* way {@code PdfCanvasCancellationTest} adapts it: a cancel reported at any
|
|
619
|
-
* other stage is retried rather than accepted. Reaching the window is
|
|
620
|
-
* ASSERTED — a run that never got there has not tested the leak.
|
|
621
|
-
*/
|
|
622
|
-
@Test
|
|
623
|
-
public void cancellingAfterTheDrawLeavesNoSlotBehind() throws Exception {
|
|
624
|
-
File pdf =
|
|
625
|
-
TestPdfs.pattern(ctx(), "jsi-direct-cancel.pdf", TestPdfs.ARCH_D_W, TestPdfs.ARCH_D_H);
|
|
626
|
-
PdfCanvasRasterizer r = PdfCanvasRasterizer.openFile(pdf, null, 1);
|
|
627
|
-
try {
|
|
628
|
-
// Big destination, because S1 finding 2 says cost tracks bitmap area.
|
|
629
|
-
RasterRequest heavy =
|
|
630
|
-
new RasterRequest(
|
|
631
|
-
0, 200f, 300f, 800f, 800f, 2f, false, RasterRequest.BACKGROUND_WHITE);
|
|
632
|
-
|
|
633
|
-
long baselineMs = Long.MAX_VALUE;
|
|
634
|
-
for (int i = 0; i < 3; i++) {
|
|
635
|
-
long t = System.nanoTime();
|
|
636
|
-
r.render(heavy, null);
|
|
637
|
-
baselineMs = Math.min(baselineMs, (System.nanoTime() - t) / 1_000_000);
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
long delayMs = Math.max(5, baselineMs / 2);
|
|
641
|
-
boolean reachedPostDraw = false;
|
|
642
|
-
|
|
643
|
-
for (int attempt = 0; attempt < 8 && !reachedPostDraw; attempt++) {
|
|
644
|
-
PdfCanvasPixelBridge.nativeReleaseAll();
|
|
645
|
-
final long id = nextSlot();
|
|
646
|
-
final RasterCancellation signal = new RasterCancellation();
|
|
647
|
-
final CountDownLatch started = new CountDownLatch(1);
|
|
648
|
-
final AtomicReference<PdfRasterException> thrown = new AtomicReference<>();
|
|
649
|
-
final AtomicReference<RasterPixels> returned = new AtomicReference<>();
|
|
650
|
-
|
|
651
|
-
Thread worker =
|
|
652
|
-
new Thread(
|
|
653
|
-
() -> {
|
|
654
|
-
started.countDown();
|
|
655
|
-
try {
|
|
656
|
-
returned.set(r.renderToNativeSlot(heavy, signal, id));
|
|
657
|
-
} catch (PdfRasterException e) {
|
|
658
|
-
thrown.set(e);
|
|
659
|
-
}
|
|
660
|
-
});
|
|
661
|
-
worker.start();
|
|
662
|
-
assertTrue(started.await(10, TimeUnit.SECONDS));
|
|
663
|
-
Thread.sleep(delayMs);
|
|
664
|
-
signal.cancel();
|
|
665
|
-
worker.join(120_000);
|
|
666
|
-
|
|
667
|
-
PdfRasterException e = thrown.get();
|
|
668
|
-
if (e == null) {
|
|
669
|
-
// Landed after the render returned: the raster is legitimately parked.
|
|
670
|
-
assertNotNull(returned.get());
|
|
671
|
-
returned.get().release();
|
|
672
|
-
delayMs = Math.max(2, delayMs / 2);
|
|
673
|
-
continue;
|
|
674
|
-
}
|
|
675
|
-
assertEquals(PdfErrorCode.CANCELLED, e.code());
|
|
676
|
-
if (!e.getMessage().contains("after render")) {
|
|
677
|
-
// Landed before the draw. Nothing was filled, so nothing can leak —
|
|
678
|
-
// but assert that too, since a slot allocated early would show here.
|
|
679
|
-
assertEquals(
|
|
680
|
-
"a cancel before the draw must not have filled a slot",
|
|
681
|
-
0,
|
|
682
|
-
PdfCanvasPixelBridge.nativeSlotCount());
|
|
683
|
-
delayMs = Math.max(delayMs * 2, delayMs + 5);
|
|
684
|
-
continue;
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
reachedPostDraw = true;
|
|
688
|
-
assertEquals(
|
|
689
|
-
"a raster cancelled AFTER it was drawn left its slot behind — nothing "
|
|
690
|
-
+ "will ever take it",
|
|
691
|
-
0,
|
|
692
|
-
PdfCanvasPixelBridge.nativeSlotCount());
|
|
693
|
-
assertNull(PdfCanvasPixelBridge.nativeTakeForTest(id));
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
assertTrue(
|
|
697
|
-
"never managed to cancel inside the render window, so the post-draw abandon "
|
|
698
|
-
+ "path was not exercised; baseline=" + baselineMs + "ms",
|
|
699
|
-
reachedPostDraw);
|
|
700
|
-
} finally {
|
|
701
|
-
r.close();
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
/**
|
|
706
|
-
* MEASUREMENT, not verification — what the one-copy fill is worth per tile.
|
|
707
|
-
*
|
|
708
|
-
* Three numbers on the same payload and the same thread: the direct fill, the
|
|
709
|
-
* two-copy store it replaces (allocate a {@code byte[]},
|
|
710
|
-
* {@code copyPixelsToBuffer} into it, then {@code nativeStore} it), and the
|
|
711
|
-
* base64 encode both replace. The DIFFERENCE between the first two is the whole
|
|
712
|
-
* claim of this change, and it is a difference of one full-raster ART
|
|
713
|
-
* allocation plus one full-raster memcpy.
|
|
714
|
-
*
|
|
715
|
-
* Logged, never asserted: a threshold would fail on a slower device for a
|
|
716
|
-
* reason that is not a bug. See {@code PdfCanvasPhaseTimingTest} for the number
|
|
717
|
-
* that actually decides whether this was worth shipping — per-tile cost inside
|
|
718
|
-
* the lane schedule, where the render executor is the serialised bottleneck.
|
|
719
|
-
*/
|
|
720
|
-
@Test
|
|
721
|
-
public void measuresTheDirectFillAgainstTheTwoCopyStoreItReplaces() throws Exception {
|
|
722
|
-
File pdf =
|
|
723
|
-
TestPdfs.pattern(ctx(), "jsi-direct-cost.pdf", TestPdfs.LETTER_W, TestPdfs.LETTER_H);
|
|
724
|
-
PdfCanvasRasterizer r = PdfCanvasRasterizer.openFile(pdf, null, 1);
|
|
725
|
-
try {
|
|
726
|
-
final float scale = 131.25f;
|
|
727
|
-
final float points = 1024f / scale;
|
|
728
|
-
RasterRequest request =
|
|
729
|
-
new RasterRequest(
|
|
730
|
-
0, 0f, 0f, points, points, scale, false, RasterRequest.BACKGROUND_WHITE);
|
|
731
|
-
|
|
732
|
-
final int iterations = 7;
|
|
733
|
-
long[] direct = new long[iterations];
|
|
734
|
-
long[] twoCopy = new long[iterations];
|
|
735
|
-
long[] encode = new long[iterations];
|
|
736
|
-
|
|
737
|
-
// Warm both paths so the first sample is not paying for class init.
|
|
738
|
-
r.render(request, null);
|
|
739
|
-
long warmId = nextSlot();
|
|
740
|
-
r.renderToNativeSlot(request, null, warmId).release();
|
|
741
|
-
|
|
742
|
-
for (int i = 0; i < iterations; i++) {
|
|
743
|
-
// The whole render is inside the clock for both, because the phase this
|
|
744
|
-
// change touches cannot be separated from it: the fill happens while the
|
|
745
|
-
// Bitmap is still alive, inside the rasterizer.
|
|
746
|
-
long id = nextSlot();
|
|
747
|
-
long t0 = System.nanoTime();
|
|
748
|
-
RasterPixels viaDirect = r.renderToNativeSlot(request, null, id);
|
|
749
|
-
direct[i] = System.nanoTime() - t0;
|
|
750
|
-
assertTrue(viaDirect.isInNativeSlot());
|
|
751
|
-
viaDirect.release();
|
|
752
|
-
|
|
753
|
-
long id2 = nextSlot();
|
|
754
|
-
long t1 = System.nanoTime();
|
|
755
|
-
RasterPixels viaHeap = r.render(request, null);
|
|
756
|
-
PdfCanvasPixelBridge.nativeStore(id2, viaHeap.bytes);
|
|
757
|
-
twoCopy[i] = System.nanoTime() - t1;
|
|
758
|
-
PdfCanvasPixelBridge.nativeRelease(id2);
|
|
759
|
-
|
|
760
|
-
long t2 = System.nanoTime();
|
|
761
|
-
String encoded = Base64.encodeToString(viaHeap.bytes, Base64.NO_WRAP);
|
|
762
|
-
encode[i] = System.nanoTime() - t2;
|
|
763
|
-
assertTrue(encoded.length() > 0);
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
java.util.Arrays.sort(direct);
|
|
767
|
-
java.util.Arrays.sort(twoCopy);
|
|
768
|
-
java.util.Arrays.sort(encode);
|
|
769
|
-
Log.i(
|
|
770
|
-
TAG,
|
|
771
|
-
String.format(
|
|
772
|
-
"PDFCANVAS-DIRECT-COST 1024x1024 render+directFill=%.2f ms"
|
|
773
|
-
+ " render+heap+nativeStore=%.2f ms saved=%.2f ms"
|
|
774
|
-
+ " (base64 encode alone=%.2f ms)",
|
|
775
|
-
direct[iterations / 2] / 1e6,
|
|
776
|
-
twoCopy[iterations / 2] / 1e6,
|
|
777
|
-
(twoCopy[iterations / 2] - direct[iterations / 2]) / 1e6,
|
|
778
|
-
encode[iterations / 2] / 1e6));
|
|
779
|
-
} finally {
|
|
780
|
-
r.close();
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
}
|