Skip to content

Commit eaecd35

Browse files
committed
TIKA-4835 -- record per-stage pipes timings (StageTimings, PIPES_TIMING log) and add a property-gated temp-file spill scoreboard (SpillStats)
1 parent 26381aa commit eaecd35

14 files changed

Lines changed: 1020 additions & 13 deletions

File tree

CHANGES.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
Release 4.1.0 - unreleased
22

3+
* Pipes now records per-stage timings. Each PipesResult carries an optional
4+
StageTimings (fetch/parse/emit/server-wall nanos) stamped by the forked
5+
worker, and PipesClient emits one TSV-friendly line per parse on logger
6+
org.apache.tika.pipes.timing (INFO; off unless enabled in the parent
7+
process's log config) covering client wait, init, request serialize/write,
8+
server wait, the server stages, and client total. Overhead is negligible
9+
when the logger is disabled. Note for binary compatibility: the canonical
10+
PipesResult constructor gained the serverTimings component; the previous
11+
three-argument constructor remains (TIKA-4835).
12+
313
* The Kafka pipes iterator no longer stops at the first empty poll. A newly
414
subscribed consumer spends its first poll(s) joining the group and returns
515
empty even when the topic has a backlog, so the iterator could enqueue zero
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.tika.io;
18+
19+
import java.io.IOException;
20+
import java.nio.charset.StandardCharsets;
21+
import java.nio.file.Files;
22+
import java.nio.file.Path;
23+
import java.nio.file.Paths;
24+
import java.util.ArrayList;
25+
import java.util.List;
26+
import java.util.Map;
27+
import java.util.concurrent.ConcurrentHashMap;
28+
import java.util.concurrent.atomic.AtomicLong;
29+
import java.util.stream.Collectors;
30+
31+
/**
32+
* Diagnostic scoreboard of temp-file spills by calling site. Enabled only when the system
33+
* property {@code tika.debug.spillStats} names an output file; otherwise every call is a
34+
* single volatile read. The forked pipes worker may be hard-killed, so the summary is
35+
* rewritten every {@link #DUMP_EVERY} spills as well as at shutdown.
36+
*/
37+
public final class SpillStats {
38+
39+
public static final String PROP = "tika.debug.spillStats";
40+
private static final int DUMP_EVERY = 100;
41+
private static final int FRAMES = 4;
42+
43+
private static final Path OUT;
44+
private static final Map<String, long[]> SITES = new ConcurrentHashMap<>();
45+
private static final AtomicLong TOTAL_FILES = new AtomicLong();
46+
private static final AtomicLong TOTAL_BYTES = new AtomicLong();
47+
48+
static {
49+
String p = System.getProperty(PROP);
50+
OUT = p == null || p.isBlank() ? null : Paths.get(p);
51+
if (OUT != null) {
52+
Runtime.getRuntime().addShutdownHook(new Thread(SpillStats::dump, "spill-stats-dump"));
53+
}
54+
}
55+
56+
private SpillStats() {
57+
}
58+
59+
public static boolean enabled() {
60+
return OUT != null;
61+
}
62+
63+
/** Captures the calling site; returns the key to pass to {@link #recordDelete}. */
64+
public static String recordCreate() {
65+
// key = the io-layer spill site, then the first FRAMES frames outside org.apache.tika.io
66+
// (the parser/detector that forced the spill); io-internal plumbing frames are skipped.
67+
List<StackTraceElement> frames = StackWalker.getInstance().walk(s -> s
68+
.map(StackWalker.StackFrame::toStackTraceElement)
69+
.filter(f -> f.getClassName().startsWith("org.apache.tika."))
70+
.filter(f -> !f.getClassName().endsWith("TemporaryResources")
71+
&& !f.getClassName().endsWith("SpillStats"))
72+
.collect(Collectors.toList()));
73+
StringBuilder sb = new StringBuilder();
74+
int outside = 0;
75+
for (int i = 0; i < frames.size() && outside < FRAMES; i++) {
76+
StackTraceElement f = frames.get(i);
77+
boolean io = f.getClassName().startsWith("org.apache.tika.io.");
78+
if (i == 0 || !io) {
79+
if (sb.length() > 0) {
80+
sb.append('<');
81+
}
82+
sb.append(shortName(f.getClassName())).append('.').append(f.getMethodName());
83+
if (!io) {
84+
outside++;
85+
}
86+
}
87+
}
88+
String site = sb.toString();
89+
SITES.computeIfAbsent(site, k -> new long[2])[0]++;
90+
return site;
91+
}
92+
93+
public static void recordDelete(String site, Path path) {
94+
long size;
95+
try {
96+
size = Files.size(path);
97+
} catch (IOException e) {
98+
return;
99+
}
100+
SITES.computeIfAbsent(site, k -> new long[2])[1] += size;
101+
TOTAL_BYTES.addAndGet(size);
102+
if (TOTAL_FILES.incrementAndGet() % DUMP_EVERY == 0) {
103+
dump();
104+
}
105+
}
106+
107+
private static String shortName(String cls) {
108+
return cls.substring(cls.lastIndexOf('.') + 1);
109+
}
110+
111+
static synchronized void dump() {
112+
List<String> lines = new ArrayList<>();
113+
lines.add("# spill stats pid=" + ProcessHandle.current().pid() + " files=" + TOTAL_FILES.get()
114+
+ " bytes=" + TOTAL_BYTES.get());
115+
lines.add("bytes\tcount\tsite");
116+
SITES.entrySet().stream()
117+
.sorted((a, b) -> Long.compare(b.getValue()[1], a.getValue()[1]))
118+
.forEach(e -> lines.add(e.getValue()[1] + "\t" + e.getValue()[0] + "\t" + e.getKey()));
119+
try {
120+
Path out = OUT.resolveSibling(OUT.getFileName() + "." + ProcessHandle.current().pid());
121+
Files.write(out, lines, StandardCharsets.UTF_8);
122+
} catch (IOException e) {
123+
// diagnostics only; never fail the parse
124+
}
125+
}
126+
}

tika-core/src/main/java/org/apache/tika/io/TemporaryResources.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,12 @@ public Path createTempFile(String suffix) throws IOException {
8888

8989
final Path path = tempFileDir == null ? Files.createTempFile("apache-tika-", actualSuffix) :
9090
Files.createTempFile(tempFileDir, "apache-tika-", actualSuffix);
91+
final String site = SpillStats.enabled() ? SpillStats.recordCreate() : null;
9192
addResource(() -> {
9293
try {
94+
if (site != null) {
95+
SpillStats.recordDelete(site, path);
96+
}
9397
Files.delete(path);
9498
} catch (IOException e) {
9599
// delete when exit if current delete fail

tika-pipes/tika-pipes-api/src/main/java/org/apache/tika/pipes/api/PipesResult.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020

2121
import org.apache.tika.pipes.api.emitter.EmitData;
2222

23-
public record PipesResult(RESULT_STATUS status, EmitData emitData, String message) implements Serializable {
23+
public record PipesResult(RESULT_STATUS status, EmitData emitData, String message,
24+
StageTimings serverTimings) implements Serializable {
2425

2526
/**
2627
* High-level categorization of result statuses.
@@ -187,15 +188,27 @@ public byte getByte() {
187188
}
188189

189190
public PipesResult(RESULT_STATUS status) {
190-
this(status, null, null);
191+
this(status, null, null, null);
191192
}
192193

193194
public PipesResult(RESULT_STATUS status, EmitData emitData) {
194-
this(status, emitData, null);
195+
this(status, emitData, null, null);
195196
}
196197

197198
public PipesResult(RESULT_STATUS status, String message) {
198-
this(status, null, message);
199+
this(status, null, message, null);
200+
}
201+
202+
public PipesResult(RESULT_STATUS status, EmitData emitData, String message) {
203+
this(status, emitData, message, null);
204+
}
205+
206+
/**
207+
* Returns a copy of this result with the given server timings attached.
208+
* Used on the server side to stamp timings onto a result before sending FINISHED.
209+
*/
210+
public PipesResult withServerTimings(StageTimings timings) {
211+
return new PipesResult(status, emitData, message, timings);
199212
}
200213

201214
/**
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.tika.pipes.api;
18+
19+
import java.io.Serializable;
20+
21+
/**
22+
* Server-side per-stage timings attached to a {@link PipesResult} for the
23+
* structured timing log.
24+
* <p>
25+
* Values are nanoseconds; -1 indicates the stage did not run (e.g., emit was
26+
* skipped for a passback result, or fetch failed before parse started).
27+
* <p>
28+
* Failure paths (OOM, TIMEOUT, UNSPECIFIED_CRASH) generally do not produce a
29+
* normal FINISHED message and therefore carry no server timings — only
30+
* client-side timings will be available for those parses.
31+
*/
32+
public record StageTimings(long fetchNanos, long parseNanos, long emitNanos,
33+
long serverWallNanos) implements Serializable {
34+
35+
public static final long NOT_RUN = -1L;
36+
}

0 commit comments

Comments
 (0)