Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ae33e18a authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add ProtoLogConfigurationService interface for better testability." into main

parents f2e3e9fe 9bc2965a
Loading
Loading
Loading
Loading
+4 −4
Original line number Diff line number Diff line
@@ -223,17 +223,17 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto
                    "ServiceManager returned a null ProtoLog Configuration Service");

            try {
                var args = new ProtoLogConfigurationService.RegisterClientArgs();
                var args = new ProtoLogConfigurationServiceImpl.RegisterClientArgs();

                if (viewerConfigFilePath != null) {
                    args.setViewerConfigFile(viewerConfigFilePath);
                }

                final var groupArgs = Stream.of(groups)
                        .map(group -> new ProtoLogConfigurationService.RegisterClientArgs
                        .map(group -> new ProtoLogConfigurationServiceImpl.RegisterClientArgs
                                .GroupConfig(group.name(), group.isLogToLogcat()))
                        .toArray(
                                ProtoLogConfigurationService.RegisterClientArgs.GroupConfig[]::new);
                        .toArray(ProtoLogConfigurationServiceImpl
                                .RegisterClientArgs.GroupConfig[]::new);
                args.setGroups(groupArgs);

                mProtoLogConfigurationService.registerClient(this, args);
+11 −413
Original line number Diff line number Diff line
@@ -16,434 +16,32 @@

package com.android.internal.protolog;

import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.GROUPS;
import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.Group.ID;
import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.Group.NAME;
import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.Group.TAG;
import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MESSAGES;
import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.GROUP_ID;
import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.LEVEL;
import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.LOCATION;
import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.MESSAGE;
import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.MESSAGE_ID;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemService;
import android.content.Context;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ShellCallback;
import android.tracing.perfetto.DataSourceParams;
import android.tracing.perfetto.InitArguments;
import android.tracing.perfetto.Producer;
import android.util.Log;
import android.util.proto.ProtoInputStream;
import android.util.proto.ProtoOutputStream;

import com.android.internal.annotations.VisibleForTesting;

import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

/**
 * The ProtoLog service is responsible for orchestrating centralized actions of the protolog tracing
 * system. Currently this service has the following roles:
 * - Handle shell commands to toggle logging ProtoLog messages for specified groups to logcat.
 * - Handle viewer config dumping (the mapping from message hash to message string) for all protolog
 *   clients. This is for two reasons: firstly, because client processes might be frozen so might
 *   not response to the request to dump their viewer config when the trace is stopped; secondly,
 *   multiple processes might be running the same code with the same viewer config, this centralized
 *   service ensures we don't dump the same viewer config multiple times across processes.
 * <p>
 * {@link com.android.internal.protolog.IProtoLogClient ProtoLog clients} register themselves to
 * this service on initialization.
 * <p>
 * This service is intended to run on the system server, such that it never gets frozen.
 */
@SystemService(Context.PROTOLOG_CONFIGURATION_SERVICE)
public final class ProtoLogConfigurationService extends IProtoLogConfigurationService.Stub {
    private static final String LOG_TAG = "ProtoLogConfigurationService";

    private final ProtoLogDataSource mDataSource;

    /**
     * Keeps track of how many of each viewer config file is currently registered.
     * Use to keep track of which viewer config files are actively being used in tracing and might
     * need to be dumped on flush.
     */
    private final Map<String, Integer> mConfigFileCounts = new HashMap<>();
    /**
     * Keeps track of the viewer config file of each client if available.
     */
    private final Map<IProtoLogClient, String> mClientConfigFiles = new HashMap<>();

    /**
     * Keeps track of all the protolog groups that have been registered by clients and are still
     * being actively traced.
     */
    private final Set<String> mRegisteredGroups = new HashSet<>();
    /**
     * Keeps track of all the clients that are actively tracing a given protolog group.
     */
    private final Map<String, Set<IProtoLogClient>> mGroupToClients = new HashMap<>();

    /**
     * Keeps track of whether or not a given group should be logged to logcat.
     * True when logging to logcat, false otherwise.
     */
    private final Map<String, Boolean> mLogGroupToLogcatStatus = new TreeMap<>();

public interface ProtoLogConfigurationService extends IProtoLogConfigurationService {
    /**
     * Keeps track of all the tracing instance ids that are actively running for ProtoLog.
     */
    private final Set<Integer> mRunningInstances = new HashSet<>();

    private final ViewerConfigFileTracer mViewerConfigFileTracer;

    public ProtoLogConfigurationService() {
        this(ProtoLogDataSource::new, ProtoLogConfigurationService::dumpTransitionTraceConfig);
    }

    @VisibleForTesting
    public ProtoLogConfigurationService(@NonNull ProtoLogDataSourceBuilder dataSourceBuilder) {
        this(dataSourceBuilder, ProtoLogConfigurationService::dumpTransitionTraceConfig);
    }

    @VisibleForTesting
    public ProtoLogConfigurationService(@NonNull ViewerConfigFileTracer tracer) {
        this(ProtoLogDataSource::new, tracer);
    }

    @VisibleForTesting
    public ProtoLogConfigurationService(
            @NonNull ProtoLogDataSourceBuilder dataSourceBuilder,
            @NonNull ViewerConfigFileTracer tracer) {
        mDataSource = dataSourceBuilder.build(
            this::onTracingInstanceStart,
            this::onTracingInstanceFlush,
            this::onTracingInstanceStop
        );

        // Initialize the Perfetto producer and register the Perfetto ProtoLog datasource to be
        // receive the lifecycle callbacks of the datasource and write the viewer configs if and
        // when required to the datasource.
        Producer.init(InitArguments.DEFAULTS);
        final var params = new DataSourceParams.Builder()
                .setBufferExhaustedPolicy(DataSourceParams.PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_DROP)
                .build();
        mDataSource.register(params);

        mViewerConfigFileTracer = tracer;
    }

    public static class RegisterClientArgs extends IRegisterClientArgs.Stub {
        /**
         * The viewer config file to be registered for this client ProtoLog process.
         */
        @Nullable
        private String mViewerConfigFile = null;
        /**
         * The list of all groups that this client protolog process supports and might trace.
         */
        @NonNull
        private String[] mGroups = new String[0];
        /**
         * The default logcat status of the ProtoLog client. True is logging to logcat, false
         * otherwise. The indices should match the indices in {@link mGroups}.
         */
        @NonNull
        private boolean[] mLogcatStatus = new boolean[0];

        public record GroupConfig(@NonNull String group, boolean logToLogcat) {}

        /**
         * Specify groups to register with this client that will be used for protologging in this
         * process.
         * @param groups to register with this client.
         * @return self
         */
        public RegisterClientArgs setGroups(GroupConfig... groups) {
            mGroups = new String[groups.length];
            mLogcatStatus = new boolean[groups.length];

            for (int i = 0; i < groups.length; i++) {
                mGroups[i] = groups[i].group;
                mLogcatStatus[i] = groups[i].logToLogcat;
            }

            return this;
        }

        /**
         * Set the viewer config file that the logs in this process are using.
         * @param viewerConfigFile The file path of the viewer config.
         * @return self
     * Get the list of groups clients have registered to the protolog service.
     * @return The list of ProtoLog groups registered with this service.
     */
        public RegisterClientArgs setViewerConfigFile(@NonNull String viewerConfigFile) {
            mViewerConfigFile = viewerConfigFile;

            return this;
        }

        @Override
    @NonNull
        public String[] getGroups() {
            return mGroups;
        }

        @Override
        @NonNull
        public boolean[] getGroupsDefaultLogcatStatus() {
            return mLogcatStatus;
        }

        @Nullable
        @Override
        public String getViewerConfigFile() {
            return mViewerConfigFile;
        }
    }

    @FunctionalInterface
    public interface ViewerConfigFileTracer {
        /**
         * Write the viewer config data to the trace buffer.
         *
         * @param dataSource The target datasource to write the viewer config to.
         * @param viewerConfigFilePath The path of the viewer config file which contains the data we
         *                             want to write to the trace buffer.
         * @throws FileNotFoundException if the viewerConfigFilePath is invalid.
         */
        void trace(@NonNull ProtoLogDataSource dataSource, @NonNull String viewerConfigFilePath);
    }

    @Override
    public void registerClient(@NonNull IProtoLogClient client, @NonNull IRegisterClientArgs args)
            throws RemoteException {
        client.asBinder().linkToDeath(() -> onClientBinderDeath(client), /* flags */ 0);

        final String viewerConfigFile = args.getViewerConfigFile();
        if (viewerConfigFile != null) {
            registerViewerConfigFile(client, viewerConfigFile);
        }

        registerGroups(client, args.getGroups(), args.getGroupsDefaultLogcatStatus());
    }

    @Override
    public void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out,
            @Nullable FileDescriptor err, @NonNull String[] args, @Nullable ShellCallback callback,
            @NonNull ResultReceiver resultReceiver) throws RemoteException {
        new ProtoLogCommandHandler(this)
                .exec(this, in, out, err, args, callback, resultReceiver);
    }
    String[] getGroups();

    /**
     * Get the list of groups clients have registered to the protolog service.
     * @return The list of ProtoLog groups registered with this service.
     * Check if a group is logging to logcat
     * @param group The group we want to check for
     * @return True iff we are logging this group to logcat.
     */
    @NonNull
    public String[] getGroups() {
        return mRegisteredGroups.toArray(new String[0]);
    }
    boolean isLoggingToLogcat(@NonNull String group);

    /**
     * Enable logging target groups to logcat.
     * @param groups we want to enable logging them to logcat for.
     */
    public void enableProtoLogToLogcat(String... groups) {
        toggleProtoLogToLogcat(true, groups);
    }
    void enableProtoLogToLogcat(@NonNull String... groups);

    /**
     * Disable logging target groups to logcat.
     * @param groups we want to disable from being logged to logcat.
     */
    public void disableProtoLogToLogcat(String... groups) {
        toggleProtoLogToLogcat(false, groups);
    }

    /**
     * Check if a group is logging to logcat
     * @param group The group we want to check for
     * @return True iff we are logging this group to logcat.
     */
    public boolean isLoggingToLogcat(@NonNull String group) {
        final Boolean isLoggingToLogcat = mLogGroupToLogcatStatus.get(group);

        if (isLoggingToLogcat == null) {
            throw new RuntimeException(
                    "Trying to get logcat logging status of non-registered group " + group);
        }

        return isLoggingToLogcat;
    }

    private void registerViewerConfigFile(
            @NonNull IProtoLogClient client, @NonNull String viewerConfigFile) {
        final var count = mConfigFileCounts.getOrDefault(viewerConfigFile, 0);
        mConfigFileCounts.put(viewerConfigFile, count + 1);
        mClientConfigFiles.put(client, viewerConfigFile);
    }

    private void registerGroups(@NonNull IProtoLogClient client, @NonNull String[] groups,
            @NonNull boolean[] logcatStatuses) throws RemoteException {
        if (groups.length != logcatStatuses.length) {
            throw new RuntimeException(
                    "Expected groups and logcatStatuses to have the same length, "
                        + "but groups has length " + groups.length
                        + " and logcatStatuses has length " + logcatStatuses.length);
        }

        for (int i = 0; i < groups.length; i++) {
            String group = groups[i];
            boolean logcatStatus = logcatStatuses[i];

            mRegisteredGroups.add(group);

            mGroupToClients.putIfAbsent(group, new HashSet<>());
            mGroupToClients.get(group).add(client);

            if (!mLogGroupToLogcatStatus.containsKey(group)) {
                mLogGroupToLogcatStatus.put(group, logcatStatus);
            }

            boolean requestedLogToLogcat = mLogGroupToLogcatStatus.get(group);
            if (requestedLogToLogcat != logcatStatus) {
                client.toggleLogcat(requestedLogToLogcat, new String[] { group });
            }
        }
    }

    private void toggleProtoLogToLogcat(boolean enabled, @NonNull String[] groups) {
        final var clientToGroups = new HashMap<IProtoLogClient, Set<String>>();

        for (String group : groups) {
            final var clients = mGroupToClients.get(group);

            if (clients == null) {
                // No clients associated to this group
                Log.w(LOG_TAG, "Attempting to toggle log to logcat for group " + group
                        + " with no registered clients.");
                continue;
            }

            for (IProtoLogClient client : clients) {
                clientToGroups.putIfAbsent(client, new HashSet<>());
                clientToGroups.get(client).add(group);
            }
        }

        for (IProtoLogClient client : clientToGroups.keySet()) {
            try {
                client.toggleLogcat(enabled, clientToGroups.get(client).toArray(new String[0]));
            } catch (RemoteException e) {
                throw new RuntimeException(
                        "Failed to toggle logcat status for groups on client", e);
            }
        }

        for (String group : groups) {
            mLogGroupToLogcatStatus.put(group, enabled);
        }
    }

    private void onTracingInstanceStart(int instanceIdx, ProtoLogDataSource.ProtoLogConfig config) {
        mRunningInstances.add(instanceIdx);
    }

    private void onTracingInstanceFlush() {
        for (String fileName : mConfigFileCounts.keySet()) {
            mViewerConfigFileTracer.trace(mDataSource, fileName);
        }
    }

    private void onTracingInstanceStop(int instanceIdx, ProtoLogDataSource.ProtoLogConfig config) {
        mRunningInstances.remove(instanceIdx);
    }

    private static void dumpTransitionTraceConfig(@NonNull ProtoLogDataSource dataSource,
            @NonNull String viewerConfigFilePath) {
        Utils.dumpViewerConfig(dataSource, () -> {
            try {
                return new ProtoInputStream(new FileInputStream(viewerConfigFilePath));
            } catch (FileNotFoundException e) {
                throw new RuntimeException(
                        "Failed to load viewer config file " + viewerConfigFilePath, e);
            }
        });
    }

    private void onClientBinderDeath(@NonNull IProtoLogClient client) {
        // Dump the tracing config now if no other client is going to dump the same config file.
        String configFile = mClientConfigFiles.get(client);
        if (configFile != null) {
            final var newCount = mConfigFileCounts.get(configFile) - 1;
            mConfigFileCounts.put(configFile, newCount);
            boolean lastProcessWithViewerConfig = newCount == 0;
            if (lastProcessWithViewerConfig) {
                mViewerConfigFileTracer.trace(mDataSource, configFile);
            }
        }
    }

    private static void writeViewerConfigGroup(
            @NonNull ProtoInputStream pis, @NonNull ProtoOutputStream os) throws IOException {
        final long inGroupToken = pis.start(GROUPS);
        final long outGroupToken = os.start(GROUPS);

        while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
            switch (pis.getFieldNumber()) {
                case (int) ID -> {
                    int id = pis.readInt(ID);
                    os.write(ID, id);
                }
                case (int) NAME -> {
                    String name = pis.readString(NAME);
                    os.write(NAME, name);
                }
                case (int) TAG -> {
                    String tag = pis.readString(TAG);
                    os.write(TAG, tag);
                }
                default ->
                    throw new RuntimeException(
                            "Unexpected field id " + pis.getFieldNumber());
            }
        }

        pis.end(inGroupToken);
        os.end(outGroupToken);
    }

    private static void writeViewerConfigMessage(
            @NonNull ProtoInputStream pis, @NonNull ProtoOutputStream os) throws IOException {
        final long inMessageToken = pis.start(MESSAGES);
        final long outMessagesToken = os.start(MESSAGES);

        while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
            switch (pis.getFieldNumber()) {
                case (int) MESSAGE_ID -> os.write(MESSAGE_ID,
                        pis.readLong(MESSAGE_ID));
                case (int) MESSAGE -> os.write(MESSAGE, pis.readString(MESSAGE));
                case (int) LEVEL -> os.write(LEVEL, pis.readInt(LEVEL));
                case (int) GROUP_ID -> os.write(GROUP_ID, pis.readInt(GROUP_ID));
                case (int) LOCATION -> os.write(LOCATION, pis.readString(LOCATION));
                default ->
                    throw new RuntimeException(
                            "Unexpected field id " + pis.getFieldNumber());
            }
        }

        pis.end(inMessageToken);
        os.end(outMessagesToken);
    }
    void disableProtoLogToLogcat(@NonNull String... groups);
}
+454 −0

File added.

Preview size limit exceeded, changes collapsed.

+2 −2
Original line number Diff line number Diff line
@@ -107,7 +107,7 @@ import com.android.internal.os.BinderInternal;
import com.android.internal.os.RuntimeInit;
import com.android.internal.policy.AttributeCache;
import com.android.internal.protolog.ProtoLog;
import com.android.internal.protolog.ProtoLogConfigurationService;
import com.android.internal.protolog.ProtoLogConfigurationServiceImpl;
import com.android.internal.protolog.ProtoLogGroup;
import com.android.internal.util.ConcurrentUtils;
import com.android.internal.util.EmergencyAffordanceManager;
@@ -1097,7 +1097,7 @@ public final class SystemServer implements Dumpable {
        if (android.tracing.Flags.clientSideProtoLogging()) {
            t.traceBegin("StartProtoLogConfigurationService");
            ServiceManager.addService(
                    Context.PROTOLOG_CONFIGURATION_SERVICE, new ProtoLogConfigurationService());
                    Context.PROTOLOG_CONFIGURATION_SERVICE, new ProtoLogConfigurationServiceImpl());
            t.traceEnd();
        }

+3 −2
Original line number Diff line number Diff line
@@ -42,7 +42,7 @@ import android.util.proto.ProtoInputStream;

import androidx.test.platform.app.InstrumentationRegistry;

import com.android.internal.protolog.ProtoLogConfigurationService.ViewerConfigFileTracer;
import com.android.internal.protolog.ProtoLogConfigurationServiceImpl.ViewerConfigFileTracer;
import com.android.internal.protolog.common.IProtoLogGroup;
import com.android.internal.protolog.common.LogDataType;
import com.android.internal.protolog.common.LogLevel;
@@ -166,7 +166,8 @@ public class PerfettoProtoLogImplTest {
                return new ProtoInputStream(sViewerConfigBuilder.build().toByteArray());
            });
        };
        sProtoLogConfigurationService = new ProtoLogConfigurationService(dataSourceBuilder, tracer);
        sProtoLogConfigurationService =
                new ProtoLogConfigurationServiceImpl(dataSourceBuilder, tracer);

        if (android.tracing.Flags.clientSideProtoLogging()) {
            sProtoLog = new PerfettoProtoLogImpl(
Loading