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

Commit 1ea5f812 authored by Dmitri Plotnikov's avatar Dmitri Plotnikov
Browse files

Add random noise generator for Power Monitor API

Bug: 273310268
Test: atest FrameworksServicesTests:IntervalRandomNoiseGeneratorTest

Change-Id: Icf61ba54096d398e0d3722d05be7e2a196670936
parent 38175436
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -185,6 +185,7 @@ java_library_static {
        "com.android.sysprop.watchdog",
        "ImmutabilityAnnotation",
        "securebox",
        "apache-commons-math",
    ],
    javac_shard_size: 50,
    javacflags: [
+83 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * 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.android.server.powerstats;

import com.android.internal.annotations.VisibleForTesting;

import org.apache.commons.math.MathException;
import org.apache.commons.math.distribution.AbstractContinuousDistribution;
import org.apache.commons.math.distribution.BetaDistributionImpl;

import java.util.Arrays;

/**
 * Adds random noise to provided value, keeping it within the limits of a specified range.
 * @hide
 */
public class IntervalRandomNoiseGenerator {
    private static final int DISTRIBUTION_SAMPLE_SIZE = 17;

    private final AbstractContinuousDistribution mDistribution;
    private final double[] mSamples = new double[DISTRIBUTION_SAMPLE_SIZE];

    private static final double UNINITIALIZED = -1;

    /**
     * Higher alpha makes the distribution more asymmetrical, tightening it
     * closer to the high bound.  A value of alpha should be > 1 to ensure
     * that the samples closer to 1 appear more frequently t those closer
     * to 0.
     */
    IntervalRandomNoiseGenerator(double alpha) {
        if (alpha <= 1) {
            throw new IllegalArgumentException("alpha should be > 1");
        }
        mDistribution = new BetaDistributionImpl(alpha, 1 /* beta */);
        refresh();
    }

    @VisibleForTesting
    void reseed(long seed) {
        mDistribution.reseedRandomGenerator(seed);
    }

    /**
     * Returns a random value between the specified bounds, statistically closer to the
     * highProbabilityBound.
     *
     * The same value is returned for a given stickyKey until {@link #refresh()} is called.
     */
    long addNoise(long lowProbabilityBound, long highProbabilityBound, int stickyKey) {
        double sample = mSamples[stickyKey % DISTRIBUTION_SAMPLE_SIZE];
        if (sample < 0) {   // UNINITIALIZED
            try {
                sample = mDistribution.sample();
            } catch (MathException e) {
                throw new IllegalStateException(e);
            }
            mSamples[stickyKey % DISTRIBUTION_SAMPLE_SIZE] = sample;
        }
        return lowProbabilityBound + (long) ((highProbabilityBound - lowProbabilityBound) * sample);
    }

    /**
     * Resets the cache of random samples.
     */
    void refresh() {
        Arrays.fill(mSamples, UNINITIALIZED);
    }
}
+108 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * 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.android.server.powerstats;

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;

import org.jetbrains.annotations.NotNull;
import org.junit.Test;

import java.util.function.Supplier;

public class IntervalRandomNoiseGeneratorTest {

    @Test
    public void parameterizedDistribution() {
         // Assert closeness to theoretical distribution
        assertDistribution(3.0,
                0.0392,
                0.2617,
                0.6990);

        assertDistribution(5.0,
                0.0003,
                0.0098,
                0.0676,
                0.2502,
                0.6720);

        assertDistribution(9.0,
                0.0000,
                0.0002,
                0.0097,
                0.1242,
                0.8658);
    }

    private void assertDistribution(double alpha, Double... expectedBuckets) {
        IntervalRandomNoiseGenerator generator = new IntervalRandomNoiseGenerator(alpha);
        generator.reseed(42);  // Make test repeatable
        final int sampleCount = 1000;
        final int bucketCount = expectedBuckets.length;
        int[] histogram = buildHistogram(() -> {
            generator.refresh();
            return generator.addNoise(100, 200, 12345);
        }, sampleCount, bucketCount, 100, 200);

        for (int i = 0; i < expectedBuckets.length; i++) {
            assertWithMessage("Bucket #" + i)
                    .that((double) histogram[i] / sampleCount)
                    .isWithin(0.05)
                    .of(expectedBuckets[i]);
        }
    }

    @NotNull
    private int[] buildHistogram(Supplier<Long> generator, int sampleCount,
            int bucketCount, int lowBound, int highBound) {
        int[] buckets = new int[bucketCount];
        for (int i = 0; i < sampleCount; i++) {
            long sample = generator.get();
            assertThat(sample).isAtLeast(lowBound);
            assertThat(sample).isAtMost(highBound);
            buckets[(int) ((double) (sample - lowBound) / (highBound - lowBound) * bucketCount)]++;
        }
        return buckets;
    }

    @Test
    public void stickiness() {
        IntervalRandomNoiseGenerator generator = new IntervalRandomNoiseGenerator(9);
        generator.reseed(42);  // Make test repeatable

        long value1a = generator.addNoise(1000, 5000, 123);
        long value1b = generator.addNoise(1000, 5000, 123);
        long value1c = generator.addNoise(1000, 5000, 123);
        assertThat(value1b).isEqualTo(value1a);
        assertThat(value1c).isEqualTo(value1a);

        // Different stickyKey
        long value2a = generator.addNoise(1000, 5000, 321);
        long value2b = generator.addNoise(1000, 5000, 321);
        assertThat(value2a).isNotEqualTo(value1a);
        assertThat(value2b).isEqualTo(value2a);

        generator.refresh();

        // Same stickyKey after a refresh - different value
        long value3 = generator.addNoise(1000, 5000, 123);
        long value4 = generator.addNoise(1000, 5000, 321);
        assertThat(value3).isNotEqualTo(value1a);
        assertThat(value4).isNotEqualTo(value2a);
    }
}