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

Commit 9bc2965a authored by Pablo Gamito's avatar Pablo Gamito
Browse files

Add ProtoLogConfigurationService interface for better testability.

Test: TracingTests
Flag: EXEMPT minor refactor
Change-Id: If55ab7159786590b26469ccf2f3278c6534e6b8a
parent 66b87063
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