|
7 | 7 | import com.fasterxml.jackson.databind.MappingJsonFactory; |
8 | 8 | import com.fasterxml.jackson.databind.ObjectMapper; |
9 | 9 | import java.io.ByteArrayOutputStream; |
| 10 | +import java.io.FilterInputStream; |
10 | 11 | import java.io.IOException; |
11 | 12 | import java.io.InputStream; |
12 | 13 | import java.io.PrintWriter; |
@@ -953,18 +954,26 @@ private static Callable<Void> getDownloadChunkCallable( |
953 | 954 | * @throws SnowflakeSQLException |
954 | 955 | */ |
955 | 956 | private void downloadAndParseChunk(InputStream inputStream) throws SnowflakeSQLException { |
956 | | - // remember the download time |
957 | | - resultChunk.setDownloadTime(System.currentTimeMillis() - startTime); |
958 | | - downloader.addDownloadTime(resultChunk.getDownloadTime()); |
959 | | - |
960 | | - startTime = System.currentTimeMillis(); |
961 | | - |
962 | | - // parse the result json |
| 957 | + // getResultStreamProvider().getInputStream() returns once headers come back, but the |
| 958 | + // response body is streamed lazily during the parser's read*() calls below — so most of |
| 959 | + // the true download time is spent inside read(), not before it. When the metrics log is |
| 960 | + // active (debug/trace), wrap the stream in a TimingInputStream so we can split the |
| 961 | + // download/parse times accurately. Otherwise, skip the per-read instrumentation and |
| 962 | + // fall back to the cheaper, less-accurate split (treating everything before the parse |
| 963 | + // call as download and everything inside it as parse) so we don't pay the wrapping cost |
| 964 | + // when nothing reads the result. |
| 965 | + boolean splitDownloadAndParseTimes = logger.isDebugEnabled() || logger.isTraceEnabled(); |
| 966 | + long connectionSetupMs = System.currentTimeMillis() - startTime; |
| 967 | + TimingInputStream timedStream = |
| 968 | + splitDownloadAndParseTimes ? new TimingInputStream(inputStream) : null; |
| 969 | + InputStream parseStream = timedStream != null ? timedStream : inputStream; |
| 970 | + |
| 971 | + long parseStart = System.currentTimeMillis(); |
963 | 972 | try { |
964 | 973 | if (downloader.queryResultFormat == QueryResultFormat.ARROW) { |
965 | | - ((ArrowResultChunk) resultChunk).readArrowStream(inputStream); |
| 974 | + ((ArrowResultChunk) resultChunk).readArrowStream(parseStream); |
966 | 975 | } else { |
967 | | - parseJsonToChunkV2(inputStream, resultChunk); |
| 976 | + parseJsonToChunkV2(parseStream, resultChunk); |
968 | 977 | } |
969 | 978 | } catch (Exception ex) { |
970 | 979 | logger.debug( |
@@ -997,9 +1006,22 @@ private void downloadAndParseChunk(InputStream inputStream) throws SnowflakeSQLE |
997 | 1006 | } |
998 | 1007 | } |
999 | 1008 |
|
1000 | | - // add parsing time |
1001 | | - resultChunk.setParseTime(System.currentTimeMillis() - startTime); |
1002 | | - downloader.addParsingTime(resultChunk.getParseTime()); |
| 1009 | + long readPlusParseMs = System.currentTimeMillis() - parseStart; |
| 1010 | + long downloadMs; |
| 1011 | + long parseMs; |
| 1012 | + if (timedStream != null) { |
| 1013 | + long networkReadMs = TimeUnit.NANOSECONDS.toMillis(timedStream.getNanosBlocked()); |
| 1014 | + downloadMs = connectionSetupMs + networkReadMs; |
| 1015 | + parseMs = Math.max(0L, readPlusParseMs - networkReadMs); |
| 1016 | + } else { |
| 1017 | + downloadMs = connectionSetupMs; |
| 1018 | + parseMs = readPlusParseMs; |
| 1019 | + } |
| 1020 | + |
| 1021 | + resultChunk.setDownloadTime(downloadMs); |
| 1022 | + downloader.addDownloadTime(downloadMs); |
| 1023 | + resultChunk.setParseTime(parseMs); |
| 1024 | + downloader.addParsingTime(parseMs); |
1003 | 1025 | } |
1004 | 1026 |
|
1005 | 1027 | private long startTime; |
@@ -1189,4 +1211,36 @@ public DownloaderMetrics terminate() { |
1189 | 1211 | return null; |
1190 | 1212 | } |
1191 | 1213 | } |
| 1214 | + |
| 1215 | + /** |
| 1216 | + * InputStream wrapper that accumulates the time spent blocked inside read() calls. Used to |
| 1217 | + * separate true network read time from CPU-bound parsing time when reading a chunk. |
| 1218 | + */ |
| 1219 | + private static final class TimingInputStream extends FilterInputStream { |
| 1220 | + private long nanosBlocked = 0L; |
| 1221 | + |
| 1222 | + TimingInputStream(InputStream in) { |
| 1223 | + super(in); |
| 1224 | + } |
| 1225 | + |
| 1226 | + long getNanosBlocked() { |
| 1227 | + return nanosBlocked; |
| 1228 | + } |
| 1229 | + |
| 1230 | + @Override |
| 1231 | + public int read() throws IOException { |
| 1232 | + long t0 = System.nanoTime(); |
| 1233 | + int b = in.read(); |
| 1234 | + nanosBlocked += System.nanoTime() - t0; |
| 1235 | + return b; |
| 1236 | + } |
| 1237 | + |
| 1238 | + @Override |
| 1239 | + public int read(byte[] b, int off, int len) throws IOException { |
| 1240 | + long t0 = System.nanoTime(); |
| 1241 | + int n = in.read(b, off, len); |
| 1242 | + nanosBlocked += System.nanoTime() - t0; |
| 1243 | + return n; |
| 1244 | + } |
| 1245 | + } |
1192 | 1246 | } |
0 commit comments