Repository navigation
Spark 4.1: Add per-task scanDuration metric #17562
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7cc6ef4
042dab4
7b18068
9ff40e2
69aed84
901bf2b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one | ||
| * or more contributor license agreements. See the NOTICE file | ||
| * distributed with this work for additional information | ||
| * regarding copyright ownership. The ASF licenses this file | ||
| * to you under the Apache License, Version 2.0 (the | ||
| * "License"); you may not use this file except in compliance | ||
| * with the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, | ||
| * software distributed under the License is distributed on an | ||
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| * KIND, either express or implied. See the License for the | ||
| * specific language governing permissions and limitations | ||
| * under the License. | ||
| */ | ||
| package org.apache.iceberg.spark.source.metrics; | ||
|
|
||
| import java.util.Arrays; | ||
| import java.util.Locale; | ||
| import java.util.concurrent.TimeUnit; | ||
| import org.apache.spark.sql.connector.metric.CustomSumMetric; | ||
|
|
||
| public class ScanDuration extends CustomSumMetric { | ||
|
|
||
| public static final String NAME = "scanDuration"; | ||
|
|
||
| @Override | ||
| public String name() { | ||
| return NAME; | ||
| } | ||
|
|
||
| @Override | ||
| public String description() { | ||
| return "total scan duration"; | ||
| } | ||
|
|
||
| @Override | ||
| public String aggregateTaskMetrics(long[] taskMetrics) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. aggregateTaskMetrics sums per-task nanos into a single scalar, which discards straggler-task skew. Spark's own scanTime reports total (min, med, max) and you already have the full per-task array in hand - worth considering matching that shape.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed it. |
||
| if (taskMetrics.length == 0) { | ||
| return format(0L); | ||
| } | ||
|
|
||
| long totalNanos = 0L; | ||
| for (long taskMetric : taskMetrics) { | ||
| totalNanos += taskMetric; | ||
| } | ||
|
|
||
| // sort a copy: Spark reuses the array it hands us | ||
| long[] sorted = Arrays.copyOf(taskMetrics, taskMetrics.length); | ||
| Arrays.sort(sorted); | ||
|
|
||
| // mirrors MetricUtils.stringValue for Spark's own timing metrics, so a straggler task is | ||
| // visible instead of being hidden by the total | ||
| return String.format( | ||
| Locale.ROOT, | ||
| "total (min, med, max)\n%s (%s, %s, %s)", | ||
| format(totalNanos), | ||
| format(sorted[0]), | ||
| format(sorted[sorted.length / 2]), | ||
| format(sorted[sorted.length - 1])); | ||
| } | ||
|
|
||
| private String format(long nanos) { | ||
| // raw nanos are unreadable on the UI, scale to the largest meaningful unit | ||
| if (nanos < TimeUnit.MICROSECONDS.toNanos(1)) { | ||
| return nanos + " ns"; | ||
| } else if (nanos < TimeUnit.MILLISECONDS.toNanos(1)) { | ||
| return TimeUnit.NANOSECONDS.toMicros(nanos) + " us"; | ||
| } else if (nanos < TimeUnit.SECONDS.toNanos(1)) { | ||
| return TimeUnit.NANOSECONDS.toMillis(nanos) + " ms"; | ||
| } else { | ||
| return String.format(Locale.ROOT, "%.1f s", nanos / (double) TimeUnit.SECONDS.toNanos(1)); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one | ||
| * or more contributor license agreements. See the NOTICE file | ||
| * distributed with this work for additional information | ||
| * regarding copyright ownership. The ASF licenses this file | ||
| * to you under the Apache License, Version 2.0 (the | ||
| * "License"); you may not use this file except in compliance | ||
| * with the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, | ||
| * software distributed under the License is distributed on an | ||
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| * KIND, either express or implied. See the License for the | ||
| * specific language governing permissions and limitations | ||
| * under the License. | ||
| */ | ||
| package org.apache.iceberg.spark.source.metrics; | ||
|
|
||
| import org.apache.spark.sql.connector.metric.CustomTaskMetric; | ||
|
|
||
| public class TaskScanDuration implements CustomTaskMetric { | ||
|
|
||
| private final long value; | ||
|
|
||
| public TaskScanDuration(long value) { | ||
| this.value = value; | ||
| } | ||
|
|
||
| @Override | ||
| public String name() { | ||
| return ScanDuration.NAME; | ||
| } | ||
|
|
||
| @Override | ||
| public long value() { | ||
| return value; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one | ||
| * or more contributor license agreements. See the NOTICE file | ||
| * distributed with this work for additional information | ||
| * regarding copyright ownership. The ASF licenses this file | ||
| * to you under the Apache License, Version 2.0 (the | ||
| * "License"); you may not use this file except in compliance | ||
| * with the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, | ||
| * software distributed under the License is distributed on an | ||
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| * KIND, either express or implied. See the License for the | ||
| * specific language governing permissions and limitations | ||
| * under the License. | ||
| */ | ||
| package org.apache.iceberg.spark.source.metrics; | ||
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
|
|
||
| import java.util.Locale; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| class TestScanDuration { | ||
|
|
||
| @Test | ||
| void aggregateTaskMetrics() { | ||
| ScanDuration metric = new ScanDuration(); | ||
|
|
||
| assertThat(metric.aggregateTaskMetrics(new long[] {300L, 400L})) | ||
| .isEqualTo("total (min, med, max)\n700 ns (300 ns, 400 ns, 400 ns)"); | ||
| assertThat(metric.aggregateTaskMetrics(new long[] {1_500L, 2_500L})) | ||
| .isEqualTo("total (min, med, max)\n4 us (1 us, 2 us, 2 us)"); | ||
| assertThat(metric.aggregateTaskMetrics(new long[] {1_000_000L, 500_000L})) | ||
| .isEqualTo("total (min, med, max)\n1 ms (500 us, 1 ms, 1 ms)"); | ||
| assertThat(metric.aggregateTaskMetrics(new long[] {1_500_000_000L, 1_000_000_000L})) | ||
| .isEqualTo("total (min, med, max)\n2.5 s (1.0 s, 1.5 s, 1.5 s)"); | ||
| } | ||
|
|
||
| @Test | ||
| void aggregateTaskMetricsWithNoTasks() { | ||
| assertThat(new ScanDuration().aggregateTaskMetrics(new long[] {})).isEqualTo("0 ns"); | ||
| } | ||
|
|
||
| @Test | ||
| void aggregateTaskMetricsSurfacesStragglers() { | ||
| // the total alone would hide that one task took the bulk of the time | ||
| assertThat( | ||
| new ScanDuration() | ||
| .aggregateTaskMetrics(new long[] {1_000_000L, 1_000_000L, 8_000_000L})) | ||
| .isEqualTo("total (min, med, max)\n10 ms (1 ms, 1 ms, 8 ms)"); | ||
| } | ||
|
|
||
| @Test | ||
| void aggregateTaskMetricsDoesNotMutateInput() { | ||
| long[] taskMetrics = new long[] {3_000_000L, 1_000_000L, 2_000_000L}; | ||
| new ScanDuration().aggregateTaskMetrics(taskMetrics); | ||
| assertThat(taskMetrics).containsExactly(3_000_000L, 1_000_000L, 2_000_000L); | ||
| } | ||
|
|
||
| @Test | ||
| void aggregateTaskMetricsIsLocaleIndependent() { | ||
| Locale previous = Locale.getDefault(); | ||
| try { | ||
| // a comma-decimal locale would render "2,5 s" without an explicit Locale.ROOT | ||
| Locale.setDefault(Locale.GERMANY); | ||
| assertThat(new ScanDuration().aggregateTaskMetrics(new long[] {2_500_000_000L})) | ||
| .isEqualTo("total (min, med, max)\n2.5 s (2.5 s, 2.5 s, 2.5 s)"); | ||
| } finally { | ||
| Locale.setDefault(previous); | ||
| } | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the no-op
initMEtricsValuesis inherited, metrics from a later reader will override that of a previous one in the same task. Could you please fix it in a follow-up PR? Same for RowDataReader.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. With the no-op inherited, a later reader in the same task starts from zero instead of picking up the earlier reader's value, so only the last reader's duration survives. I'll fix
BatchDataReaderandRowDataReadertogether in a follow-up and link it here.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in #18726. It turned out Spark 4.2 is not affected, since it no longer calls
initMetricsValues, so the fix is 4.1 only.