| // Copyright 2022 The Bazel Authors. All rights reserved. |
| // |
| // Licensed 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 com.google.devtools.build.lib.profiler; |
| |
| import static java.util.Map.entry; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Preconditions; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.gson.stream.JsonWriter; |
| import java.io.IOException; |
| import java.time.Duration; |
| import java.util.Map; |
| import java.util.concurrent.TimeUnit; |
| import javax.annotation.Nullable; |
| |
| /** |
| * Used to inject a whole counter series (or even multiple) in one go in the JSON trace profile; |
| * these could be used to represent CPU or memory usages over the course of an invocation (as |
| * opposed to individual tasks such as executing an action). |
| */ |
| final class CounterSeriesTraceData implements TraceData { |
| @VisibleForTesting static final long PROCESS_ID = 1; |
| private final Map<ProfilerTask, double[]> counterSeriesMap; |
| private final Duration profileStart; |
| private final Duration bucketDuration; |
| private final int len; |
| private final long threadId; |
| private String displayName; |
| |
| @Nullable private String colorName; |
| |
| /** |
| * If multiple series are passed: - they will be rendered as stacked chart; - we assume they all |
| * have the same length; - display name and color are picked from the first profile task in the |
| * map. However, colors the remaining series are picked arbitrarily by the Trace renderer. |
| */ |
| CounterSeriesTraceData( |
| Map<ProfilerTask, double[]> counterSeriesMap, |
| Duration profileStart, |
| Duration bucketDuration) { |
| Integer len = null; |
| for (ProfilerTask profilerTask : counterSeriesMap.keySet()) { |
| Preconditions.checkState(COUNTER_TASK_TO_SERIES_NAME.containsKey(profilerTask)); |
| if (len == null) { |
| len = counterSeriesMap.get(profilerTask).length; |
| |
| this.displayName = profilerTask.description; |
| |
| // Pick acceptable counter colors manually, unfortunately we have to pick from these |
| // weird reserved names from |
| // https://github.com/catapult-project/catapult/blob/master/tracing/tracing/base/color_scheme.html |
| this.colorName = COUNTER_TASK_TO_COLOR.get(profilerTask); |
| } else { |
| Preconditions.checkState(len.equals(counterSeriesMap.get(profilerTask).length)); |
| } |
| } |
| this.len = len; |
| this.threadId = Thread.currentThread().getId(); |
| this.counterSeriesMap = counterSeriesMap; |
| this.profileStart = profileStart; |
| this.bucketDuration = bucketDuration; |
| } |
| |
| // Pick acceptable counter colors manually, unfortunately we have to pick from these |
| // weird reserved names from |
| // https://github.com/catapult-project/catapult/blob/master/tracing/tracing/base/color_scheme.html |
| private static final ImmutableMap<ProfilerTask, String> COUNTER_TASK_TO_COLOR = |
| ImmutableMap.ofEntries( |
| entry(ProfilerTask.LOCAL_ACTION_COUNTS, "detailed_memory_dump"), |
| entry(ProfilerTask.LOCAL_CPU_USAGE, "good"), |
| entry(ProfilerTask.SYSTEM_CPU_USAGE, "rail_load"), |
| entry(ProfilerTask.LOCAL_MEMORY_USAGE, "olive"), |
| entry(ProfilerTask.SYSTEM_MEMORY_USAGE, "bad"), |
| entry(ProfilerTask.SYSTEM_NETWORK_UP_USAGE, "rail_response"), |
| entry(ProfilerTask.SYSTEM_NETWORK_DOWN_USAGE, "rail_response"), |
| entry(ProfilerTask.WORKERS_MEMORY_USAGE, "rail_animation"), |
| entry(ProfilerTask.SYSTEM_LOAD_AVERAGE, "generic_work"), |
| entry(ProfilerTask.MEMORY_USAGE_ESTIMATION, "rail_idle"), |
| entry(ProfilerTask.CPU_USAGE_ESTIMATION, "cq_build_attempt_passed"), |
| entry(ProfilerTask.PRESSURE_STALL_FULL_IO, "rail_animation"), |
| entry(ProfilerTask.PRESSURE_STALL_SOME_IO, "cq_build_attempt_failed"), |
| entry(ProfilerTask.PRESSURE_STALL_FULL_MEMORY, "thread_state_unknown"), |
| entry(ProfilerTask.PRESSURE_STALL_SOME_MEMORY, "rail_idle"), |
| entry(ProfilerTask.PRESSURE_STALL_SOME_CPU, "thread_state_running")); |
| |
| private static final ImmutableMap<ProfilerTask, String> COUNTER_TASK_TO_SERIES_NAME = |
| ImmutableMap.ofEntries( |
| entry(ProfilerTask.ACTION_COUNTS, "action"), |
| entry(ProfilerTask.ACTION_CACHE_COUNTS, "local action cache"), |
| entry(ProfilerTask.LOCAL_ACTION_COUNTS, "local action"), |
| entry(ProfilerTask.LOCAL_CPU_USAGE, "cpu"), |
| entry(ProfilerTask.SYSTEM_CPU_USAGE, "system cpu"), |
| entry(ProfilerTask.LOCAL_MEMORY_USAGE, "memory"), |
| entry(ProfilerTask.SYSTEM_MEMORY_USAGE, "system memory"), |
| entry(ProfilerTask.SYSTEM_NETWORK_UP_USAGE, "system network up (Mbps)"), |
| entry(ProfilerTask.SYSTEM_NETWORK_DOWN_USAGE, "system network down (Mbps)"), |
| entry(ProfilerTask.WORKERS_MEMORY_USAGE, "workers memory"), |
| entry(ProfilerTask.SYSTEM_LOAD_AVERAGE, "load"), |
| entry(ProfilerTask.MEMORY_USAGE_ESTIMATION, "estimated memory"), |
| entry(ProfilerTask.CPU_USAGE_ESTIMATION, "estimated cpu"), |
| entry(ProfilerTask.PRESSURE_STALL_FULL_IO, "i/o pressure (full)"), |
| entry(ProfilerTask.PRESSURE_STALL_FULL_MEMORY, "memory pressure (full)"), |
| entry(ProfilerTask.PRESSURE_STALL_SOME_IO, "i/o pressure (some)"), |
| entry(ProfilerTask.PRESSURE_STALL_SOME_MEMORY, "memory pressure (some)"), |
| entry(ProfilerTask.PRESSURE_STALL_SOME_CPU, "cpu pressure (some)")); |
| |
| @Override |
| public void writeTraceData(JsonWriter jsonWriter, long profileStartTimeNanos) throws IOException { |
| // See |
| // https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview#heading=h.msg3086636uq |
| // for how counter series are represented in the Chrome Trace Event format. |
| boolean recorded = false; |
| for (int i = 0; i < len; i++) { |
| long timeNanos = profileStart.plus(bucketDuration.multipliedBy(i)).toNanos(); |
| jsonWriter.setIndent(" "); |
| jsonWriter.beginObject(); |
| jsonWriter.setIndent(""); |
| jsonWriter.name("name").value(displayName); |
| jsonWriter.name("pid").value(PROCESS_ID); |
| jsonWriter.name("tid").value(threadId); |
| if (colorName != null) { |
| jsonWriter.name("cname").value(colorName); |
| } |
| jsonWriter.name("ph").value("C"); |
| jsonWriter.name("ts").value(TimeUnit.NANOSECONDS.toMicros(timeNanos - profileStartTimeNanos)); |
| jsonWriter.name("args"); |
| |
| jsonWriter.beginObject(); |
| for (ProfilerTask profilerTask : counterSeriesMap.keySet()) { |
| double value = counterSeriesMap.get(profilerTask)[i]; |
| // Skip counts equal to zero. They will show up as a thin line in the profile. Once we |
| // record the profile task we need to post it until the end. |
| if (Math.abs(value) > 0.00001 || recorded) { |
| jsonWriter.name(COUNTER_TASK_TO_SERIES_NAME.get(profilerTask)).value(value); |
| recorded = true; |
| } |
| } |
| jsonWriter.endObject(); |
| |
| jsonWriter.endObject(); |
| } |
| } |
| } |