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

Commit 7fb1fa61 authored by Kweku Adams's avatar Kweku Adams Committed by Automerger Merge Worker
Browse files

Merge "Track basic TARE statistics." into tm-dev am: 5fe461a1 am: 97443b40

parents 35506db5 97443b40
Loading
Loading
Loading
Loading
+5 −2
Original line number Diff line number Diff line
@@ -73,6 +73,7 @@ class Agent {

    private final Object mLock;
    private final Handler mHandler;
    private final Analyst mAnalyst;
    private final InternalResourceService mIrs;
    private final Scribe mScribe;

@@ -110,10 +111,11 @@ class Agent {
     */
    private static final int MSG_CHECK_INDIVIDUAL_AFFORDABILITY = 1;

    Agent(@NonNull InternalResourceService irs, @NonNull Scribe scribe) {
    Agent(@NonNull InternalResourceService irs, @NonNull Scribe scribe, @NonNull Analyst analyst) {
        mLock = irs.getLock();
        mIrs = irs;
        mScribe = scribe;
        mAnalyst = analyst;
        mHandler = new AgentHandler(TareHandlerThread.get().getLooper());
        mAppStandbyInternal = LocalServices.getService(AppStandbyInternal.class);
        mBalanceThresholdAlarmQueue = new BalanceThresholdAlarmQueue(
@@ -443,7 +445,7 @@ class Agent {
    void recordTransactionLocked(final int userId, @NonNull final String pkgName,
            @NonNull Ledger ledger, @NonNull Ledger.Transaction transaction,
            final boolean notifyOnAffordabilityChange) {
        if (transaction.delta == 0) {
        if (!DEBUG && transaction.delta == 0) {
            // Skip recording transactions with a delta of 0 to save on space.
            return;
        }
@@ -471,6 +473,7 @@ class Agent {
        }
        ledger.recordTransaction(transaction);
        mScribe.adjustRemainingConsumableCakesLocked(-transaction.ctp);
        mAnalyst.noteTransaction(transaction);
        if (transaction.delta != 0 && notifyOnAffordabilityChange) {
            final ArraySet<ActionAffordabilityNote> actionAffordabilityNotes =
                    mActionAffordabilityNotes.get(userId, pkgName);
+290 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.tare;

import static com.android.server.tare.EconomicPolicy.TYPE_ACTION;
import static com.android.server.tare.EconomicPolicy.TYPE_REGULATION;
import static com.android.server.tare.EconomicPolicy.TYPE_REWARD;
import static com.android.server.tare.EconomicPolicy.getEventType;
import static com.android.server.tare.TareUtils.cakeToString;

import android.annotation.NonNull;
import android.util.IndentingPrintWriter;
import android.util.Log;

import java.util.ArrayList;
import java.util.List;

/**
 * Responsible for maintaining statistics and analysis of TARE's performance.
 */
public class Analyst {
    private static final String TAG = "TARE-" + Analyst.class.getSimpleName();
    private static final boolean DEBUG = InternalResourceService.DEBUG
            || Log.isLoggable(TAG, Log.DEBUG);

    private static final int NUM_PERIODS_TO_RETAIN = 8;

    static final class Report {
        /** How much the battery was discharged over the tracked period. */
        public int cumulativeBatteryDischarge = 0;
        public int currentBatteryLevel = 0;
        /**
         * Profit from performing actions. This excludes special circumstances where we charge the
         * app
         * less than the action's CTP.
         */
        public long cumulativeProfit = 0;
        public int numProfitableActions = 0;
        /**
         * Losses from performing actions for special circumstances (eg. for a TOP app) where we
         * charge
         * the app less than the action's CTP.
         */
        public long cumulativeLoss = 0;
        public int numUnprofitableActions = 0;
        /**
         * The total number of rewards given to apps over this period.
         */
        public long cumulativeRewards = 0;
        public int numRewards = 0;
        /**
         * Regulations that increased an app's balance.
         */
        public long cumulativePositiveRegulations = 0;
        public int numPositiveRegulations = 0;
        /**
         * Regulations that decreased an app's balance.
         */
        public long cumulativeNegativeRegulations = 0;
        public int numNegativeRegulations = 0;

        private void clear() {
            cumulativeBatteryDischarge = 0;
            currentBatteryLevel = 0;
            cumulativeProfit = 0;
            numProfitableActions = 0;
            cumulativeLoss = 0;
            numUnprofitableActions = 0;
            cumulativeRewards = 0;
            numRewards = 0;
            cumulativePositiveRegulations = 0;
            numPositiveRegulations = 0;
            cumulativeNegativeRegulations = 0;
            numNegativeRegulations = 0;
        }
    }

    private int mPeriodIndex = 0;
    /** How much the battery was discharged over the tracked period. */
    private final Report[] mReports = new Report[NUM_PERIODS_TO_RETAIN];

    /** Returns the list of most recent reports, with the oldest report first. */
    @NonNull
    List<Report> getReports() {
        final List<Report> list = new ArrayList<>(NUM_PERIODS_TO_RETAIN);
        for (int i = 1; i <= NUM_PERIODS_TO_RETAIN; ++i) {
            final int idx = (mPeriodIndex + i) % NUM_PERIODS_TO_RETAIN;
            final Report report = mReports[idx];
            if (report != null) {
                list.add(report);
            }
        }
        return list;
    }

    /**
     * Tracks the given reports instead of whatever is currently saved. Reports should be ordered
     * oldest to most recent.
     */
    void loadReports(@NonNull List<Report> reports) {
        final int numReports = reports.size();
        mPeriodIndex = Math.max(0, numReports - 1);
        for (int i = 0; i < NUM_PERIODS_TO_RETAIN; ++i) {
            if (i < numReports) {
                mReports[i] = reports.get(i);
            } else {
                mReports[i] = null;
            }
        }
    }

    void noteBatteryLevelChange(int newBatteryLevel) {
        if (newBatteryLevel == 100 && mReports[mPeriodIndex] != null
                && mReports[mPeriodIndex].currentBatteryLevel < newBatteryLevel) {
            mPeriodIndex = (mPeriodIndex + 1) % NUM_PERIODS_TO_RETAIN;
            if (mReports[mPeriodIndex] != null) {
                final Report report = mReports[mPeriodIndex];
                report.clear();
                report.currentBatteryLevel = newBatteryLevel;
                return;
            }
        }

        if (mReports[mPeriodIndex] == null) {
            Report report = new Report();
            mReports[mPeriodIndex] = report;
            report.currentBatteryLevel = newBatteryLevel;
            return;
        }

        final Report report = mReports[mPeriodIndex];
        if (newBatteryLevel < report.currentBatteryLevel) {
            report.cumulativeBatteryDischarge += (report.currentBatteryLevel - newBatteryLevel);
        }
        report.currentBatteryLevel = newBatteryLevel;
    }

    void noteTransaction(@NonNull Ledger.Transaction transaction) {
        if (mReports[mPeriodIndex] == null) {
            mReports[mPeriodIndex] = new Report();
        }
        final Report report = mReports[mPeriodIndex];
        switch (getEventType(transaction.eventId)) {
            case TYPE_ACTION:
                // For now, assume all instances where price < CTP is a special instance.
                // TODO: add an explicit signal for special circumstances
                if (-transaction.delta > transaction.ctp) {
                    report.cumulativeProfit += (-transaction.delta - transaction.ctp);
                    report.numProfitableActions++;
                } else if (-transaction.delta < transaction.ctp) {
                    report.cumulativeLoss += (transaction.ctp + transaction.delta);
                    report.numUnprofitableActions++;
                }
                break;
            case TYPE_REGULATION:
                if (transaction.delta > 0) {
                    report.cumulativePositiveRegulations += transaction.delta;
                    report.numPositiveRegulations++;
                } else if (transaction.delta < 0) {
                    report.cumulativeNegativeRegulations -= transaction.delta;
                    report.numNegativeRegulations++;
                }
                break;
            case TYPE_REWARD:
                if (transaction.delta != 0) {
                    report.cumulativeRewards += transaction.delta;
                    report.numRewards++;
                }
                break;
        }
    }

    void tearDown() {
        for (int i = 0; i < mReports.length; ++i) {
            mReports[i] = null;
        }
        mPeriodIndex = 0;
    }

    @NonNull
    private String padStringWithSpaces(@NonNull String text, int targetLength) {
        // Make sure to have at least one space on either side.
        final int padding = Math.max(2, targetLength - text.length()) >>> 1;
        return " ".repeat(padding) + text + " ".repeat(padding);
    }

    void dump(IndentingPrintWriter pw) {
        pw.println("Reports:");
        pw.increaseIndent();
        pw.print("      Total Discharge");
        final int statColsLength = 47;
        pw.print(padStringWithSpaces("Profit (avg/action : avg/discharge)", statColsLength));
        pw.print(padStringWithSpaces("Loss (avg/action : avg/discharge)", statColsLength));
        pw.print(padStringWithSpaces("Rewards (avg/reward : avg/discharge)", statColsLength));
        pw.print(padStringWithSpaces("+Regs (avg/reg : avg/discharge)", statColsLength));
        pw.print(padStringWithSpaces("-Regs (avg/reg : avg/discharge)", statColsLength));
        pw.println();
        for (int r = 0; r < NUM_PERIODS_TO_RETAIN; ++r) {
            final int idx = (mPeriodIndex - r + NUM_PERIODS_TO_RETAIN) % NUM_PERIODS_TO_RETAIN;
            final Report report = mReports[idx];
            if (report == null) {
                continue;
            }
            pw.print("t-");
            pw.print(r);
            pw.print(":  ");
            pw.print(padStringWithSpaces(Integer.toString(report.cumulativeBatteryDischarge), 15));
            if (report.numProfitableActions > 0) {
                final String perDischarge = report.cumulativeBatteryDischarge > 0
                        ? cakeToString(report.cumulativeProfit / report.cumulativeBatteryDischarge)
                        : "N/A";
                pw.print(padStringWithSpaces(String.format("%s (%s : %s)",
                                cakeToString(report.cumulativeProfit),
                                cakeToString(report.cumulativeProfit / report.numProfitableActions),
                                perDischarge),
                        statColsLength));
            } else {
                pw.print(padStringWithSpaces("N/A", statColsLength));
            }
            if (report.numUnprofitableActions > 0) {
                final String perDischarge = report.cumulativeBatteryDischarge > 0
                        ? cakeToString(report.cumulativeLoss / report.cumulativeBatteryDischarge)
                        : "N/A";
                pw.print(padStringWithSpaces(String.format("%s (%s : %s)",
                                cakeToString(report.cumulativeLoss),
                                cakeToString(report.cumulativeLoss / report.numUnprofitableActions),
                                perDischarge),
                        statColsLength));
            } else {
                pw.print(padStringWithSpaces("N/A", statColsLength));
            }
            if (report.numRewards > 0) {
                final String perDischarge = report.cumulativeBatteryDischarge > 0
                        ? cakeToString(report.cumulativeRewards / report.cumulativeBatteryDischarge)
                        : "N/A";
                pw.print(padStringWithSpaces(String.format("%s (%s : %s)",
                                cakeToString(report.cumulativeRewards),
                                cakeToString(report.cumulativeRewards / report.numRewards),
                                perDischarge),
                        statColsLength));
            } else {
                pw.print(padStringWithSpaces("N/A", statColsLength));
            }
            if (report.numPositiveRegulations > 0) {
                final String perDischarge = report.cumulativeBatteryDischarge > 0
                        ? cakeToString(
                        report.cumulativePositiveRegulations / report.cumulativeBatteryDischarge)
                        : "N/A";
                pw.print(padStringWithSpaces(String.format("%s (%s : %s)",
                                cakeToString(report.cumulativePositiveRegulations),
                                cakeToString(report.cumulativePositiveRegulations
                                        / report.numPositiveRegulations),
                                perDischarge),
                        statColsLength));
            } else {
                pw.print(padStringWithSpaces("N/A", statColsLength));
            }
            if (report.numNegativeRegulations > 0) {
                final String perDischarge = report.cumulativeBatteryDischarge > 0
                        ? cakeToString(
                        report.cumulativeNegativeRegulations / report.cumulativeBatteryDischarge)
                        : "N/A";
                pw.print(padStringWithSpaces(String.format("%s (%s : %s)",
                                cakeToString(report.cumulativeNegativeRegulations),
                                cakeToString(report.cumulativeNegativeRegulations
                                        / report.numNegativeRegulations),
                                perDischarge),
                        statColsLength));
            } else {
                pw.print(padStringWithSpaces("N/A", statColsLength));
            }
            pw.println();
        }
        pw.decreaseIndent();
    }
}
+9 −2
Original line number Diff line number Diff line
@@ -121,6 +121,7 @@ public class InternalResourceService extends SystemService {
    private IDeviceIdleController mDeviceIdleController;

    private final Agent mAgent;
    private final Analyst mAnalyst;
    private final ConfigObserver mConfigObserver;
    private final EconomyManagerStub mEconomyManagerStub;
    private final Scribe mScribe;
@@ -254,9 +255,10 @@ public class InternalResourceService extends SystemService {
        mPackageManager = context.getPackageManager();
        mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
        mEconomyManagerStub = new EconomyManagerStub();
        mScribe = new Scribe(this);
        mAnalyst = new Analyst();
        mScribe = new Scribe(this, mAnalyst);
        mCompleteEconomicPolicy = new CompleteEconomicPolicy(this);
        mAgent = new Agent(this, mScribe);
        mAgent = new Agent(this, mScribe, mAnalyst);

        mConfigObserver = new ConfigObserver(mHandler, context);

@@ -375,6 +377,7 @@ public class InternalResourceService extends SystemService {
    void onBatteryLevelChanged() {
        synchronized (mLock) {
            final int newBatteryLevel = getCurrentBatteryLevel();
            mAnalyst.noteBatteryLevelChange(newBatteryLevel);
            final boolean increased = newBatteryLevel > mCurrentBatteryLevel;
            if (increased) {
                mAgent.distributeBasicIncomeLocked(newBatteryLevel);
@@ -741,6 +744,7 @@ public class InternalResourceService extends SystemService {
        }
        synchronized (mLock) {
            mAgent.tearDownLocked();
            mAnalyst.tearDown();
            mCompleteEconomicPolicy.tearDown();
            mExemptedApps.clear();
            mExemptListLoaded = false;
@@ -1145,6 +1149,9 @@ public class InternalResourceService extends SystemService {

            pw.println();
            mAgent.dumpLocked(pw);

            pw.println();
            mAnalyst.dump(pw);
        }
    }
}
+77 −3
Original line number Diff line number Diff line
@@ -72,6 +72,7 @@ public class Scribe {
    private static final String XML_TAG_TARE = "tare";
    private static final String XML_TAG_TRANSACTION = "transaction";
    private static final String XML_TAG_USER = "user";
    private static final String XML_TAG_PERIOD_REPORT = "report";

    private static final String XML_ATTR_CTP = "ctp";
    private static final String XML_ATTR_DELTA = "delta";
@@ -86,6 +87,18 @@ public class Scribe {
    private static final String XML_ATTR_LAST_RECLAMATION_TIME = "lastReclamationTime";
    private static final String XML_ATTR_REMAINING_CONSUMABLE_CAKES = "remainingConsumableCakes";
    private static final String XML_ATTR_CONSUMPTION_LIMIT = "consumptionLimit";
    private static final String XML_ATTR_PR_DISCHARGE = "discharge";
    private static final String XML_ATTR_PR_BATTERY_LEVEL = "batteryLevel";
    private static final String XML_ATTR_PR_PROFIT = "profit";
    private static final String XML_ATTR_PR_NUM_PROFIT = "numProfits";
    private static final String XML_ATTR_PR_LOSS = "loss";
    private static final String XML_ATTR_PR_NUM_LOSS = "numLoss";
    private static final String XML_ATTR_PR_REWARDS = "rewards";
    private static final String XML_ATTR_PR_NUM_REWARDS = "numRewards";
    private static final String XML_ATTR_PR_POS_REGULATIONS = "posRegulations";
    private static final String XML_ATTR_PR_NUM_POS_REGULATIONS = "numPosRegulations";
    private static final String XML_ATTR_PR_NEG_REGULATIONS = "negRegulations";
    private static final String XML_ATTR_PR_NUM_NEG_REGULATIONS = "numNegRegulations";

    /** Version of the file schema. */
    private static final int STATE_FILE_VERSION = 0;
@@ -94,6 +107,7 @@ public class Scribe {

    private final AtomicFile mStateFile;
    private final InternalResourceService mIrs;
    private final Analyst mAnalyst;

    @GuardedBy("mIrs.getLock()")
    private long mLastReclamationTime;
@@ -107,13 +121,14 @@ public class Scribe {
    private final Runnable mCleanRunnable = this::cleanupLedgers;
    private final Runnable mWriteRunnable = this::writeState;

    Scribe(InternalResourceService irs) {
        this(irs, Environment.getDataSystemDirectory());
    Scribe(InternalResourceService irs, Analyst analyst) {
        this(irs, analyst, Environment.getDataSystemDirectory());
    }

    @VisibleForTesting
    Scribe(InternalResourceService irs, File dataDir) {
    Scribe(InternalResourceService irs, Analyst analyst, File dataDir) {
        mIrs = irs;
        mAnalyst = analyst;

        final File tareDir = new File(dataDir, "tare");
        //noinspection ResultOfMethodCallIgnored
@@ -210,6 +225,7 @@ public class Scribe {
            }
        }

        final List<Analyst.Report> reports = new ArrayList<>();
        try (FileInputStream fis = mStateFile.openRead()) {
            TypedXmlPullParser parser = Xml.resolvePullParser(fis);

@@ -263,11 +279,15 @@ public class Scribe {
                                readUserFromXmlLocked(
                                        parser, installedPackagesPerUser, endTimeCutoff));
                        break;
                    case XML_TAG_PERIOD_REPORT:
                        reports.add(readReportFromXml(parser));
                        break;
                    default:
                        Slog.e(TAG, "Unexpected tag: " + tagName);
                        break;
                }
            }
            mAnalyst.loadReports(reports);
            scheduleCleanup(earliestEndTime);
        } catch (IOException | XmlPullParserException e) {
            Slog.wtf(TAG, "Error reading state from disk", e);
@@ -457,6 +477,37 @@ public class Scribe {
        return earliestEndTime;
    }


    /**
     * @param parser Xml parser at the beginning of a {@link #XML_TAG_PERIOD_REPORT} tag. The next
     *               "parser.next()" call will take the parser into the body of the report tag.
     * @return Newly instantiated Report holding all the information we just read out of the xml tag
     */
    @NonNull
    private static Analyst.Report readReportFromXml(TypedXmlPullParser parser)
            throws XmlPullParserException, IOException {
        final Analyst.Report report = new Analyst.Report();

        report.cumulativeBatteryDischarge = parser.getAttributeInt(null, XML_ATTR_PR_DISCHARGE);
        report.currentBatteryLevel = parser.getAttributeInt(null, XML_ATTR_PR_BATTERY_LEVEL);
        report.cumulativeProfit = parser.getAttributeLong(null, XML_ATTR_PR_PROFIT);
        report.numProfitableActions = parser.getAttributeInt(null, XML_ATTR_PR_NUM_PROFIT);
        report.cumulativeLoss = parser.getAttributeLong(null, XML_ATTR_PR_LOSS);
        report.numUnprofitableActions = parser.getAttributeInt(null, XML_ATTR_PR_NUM_LOSS);
        report.cumulativeRewards = parser.getAttributeLong(null, XML_ATTR_PR_REWARDS);
        report.numRewards = parser.getAttributeInt(null, XML_ATTR_PR_NUM_REWARDS);
        report.cumulativePositiveRegulations =
                parser.getAttributeLong(null, XML_ATTR_PR_POS_REGULATIONS);
        report.numPositiveRegulations =
                parser.getAttributeInt(null, XML_ATTR_PR_NUM_POS_REGULATIONS);
        report.cumulativeNegativeRegulations =
                parser.getAttributeLong(null, XML_ATTR_PR_NEG_REGULATIONS);
        report.numNegativeRegulations =
                parser.getAttributeInt(null, XML_ATTR_PR_NUM_NEG_REGULATIONS);

        return report;
    }

    private void scheduleCleanup(long earliestEndTime) {
        if (earliestEndTime == Long.MAX_VALUE) {
            return;
@@ -501,6 +552,11 @@ public class Scribe {
                            writeUserLocked(out, userId));
                }

                List<Analyst.Report> reports = mAnalyst.getReports();
                for (int i = 0, size = reports.size(); i < size; ++i) {
                    writeReport(out, reports.get(i));
                }

                out.endTag(null, XML_TAG_TARE);

                out.endDocument();
@@ -560,6 +616,24 @@ public class Scribe {
        out.endTag(null, XML_TAG_TRANSACTION);
    }

    private static void writeReport(@NonNull TypedXmlSerializer out,
            @NonNull Analyst.Report report) throws IOException {
        out.startTag(null, XML_TAG_PERIOD_REPORT);
        out.attributeInt(null, XML_ATTR_PR_DISCHARGE, report.cumulativeBatteryDischarge);
        out.attributeInt(null, XML_ATTR_PR_BATTERY_LEVEL, report.currentBatteryLevel);
        out.attributeLong(null, XML_ATTR_PR_PROFIT, report.cumulativeProfit);
        out.attributeInt(null, XML_ATTR_PR_NUM_PROFIT, report.numProfitableActions);
        out.attributeLong(null, XML_ATTR_PR_LOSS, report.cumulativeLoss);
        out.attributeInt(null, XML_ATTR_PR_NUM_LOSS, report.numUnprofitableActions);
        out.attributeLong(null, XML_ATTR_PR_REWARDS, report.cumulativeRewards);
        out.attributeInt(null, XML_ATTR_PR_NUM_REWARDS, report.numRewards);
        out.attributeLong(null, XML_ATTR_PR_POS_REGULATIONS, report.cumulativePositiveRegulations);
        out.attributeInt(null, XML_ATTR_PR_NUM_POS_REGULATIONS, report.numPositiveRegulations);
        out.attributeLong(null, XML_ATTR_PR_NEG_REGULATIONS, report.cumulativeNegativeRegulations);
        out.attributeInt(null, XML_ATTR_PR_NUM_NEG_REGULATIONS, report.numNegativeRegulations);
        out.endTag(null, XML_TAG_PERIOD_REPORT);
    }

    @GuardedBy("mIrs.getLock()")
    void dumpLocked(IndentingPrintWriter pw, boolean dumpAll) {
        pw.println("Ledgers:");
+8 −6
Original line number Diff line number Diff line
@@ -46,6 +46,8 @@ public class AgentTest {
    @Mock
    private CompleteEconomicPolicy mEconomicPolicy;
    @Mock
    private Analyst mAnalyst;
    @Mock
    private Context mContext;
    @Mock
    private InternalResourceService mIrs;
@@ -53,8 +55,8 @@ public class AgentTest {
    private Scribe mScribe;

    private static class MockScribe extends Scribe {
        MockScribe(InternalResourceService irs) {
            super(irs);
        MockScribe(InternalResourceService irs, Analyst analyst) {
            super(irs, analyst);
        }

        @Override
@@ -74,7 +76,7 @@ public class AgentTest {
        doReturn(mEconomicPolicy).when(mIrs).getCompleteEconomicPolicyLocked();
        doReturn(mIrs).when(mIrs).getLock();
        doReturn(mock(AlarmManager.class)).when(mContext).getSystemService(Context.ALARM_SERVICE);
        mScribe = new MockScribe(mIrs);
        mScribe = new MockScribe(mIrs, mAnalyst);
    }

    @After
@@ -86,7 +88,7 @@ public class AgentTest {

    @Test
    public void testRecordTransaction_UnderMax() {
        Agent agent = new Agent(mIrs, mScribe);
        Agent agent = new Agent(mIrs, mScribe, mAnalyst);
        Ledger ledger = new Ledger();

        doReturn(1_000_000L).when(mIrs).getConsumptionLimitLocked();
@@ -115,7 +117,7 @@ public class AgentTest {

    @Test
    public void testRecordTransaction_MaxConsumptionLimit() {
        Agent agent = new Agent(mIrs, mScribe);
        Agent agent = new Agent(mIrs, mScribe, mAnalyst);
        Ledger ledger = new Ledger();

        doReturn(1000L).when(mIrs).getConsumptionLimitLocked();
@@ -162,7 +164,7 @@ public class AgentTest {

    @Test
    public void testRecordTransaction_MaxSatiatedBalance() {
        Agent agent = new Agent(mIrs, mScribe);
        Agent agent = new Agent(mIrs, mScribe, mAnalyst);
        Ledger ledger = new Ledger();

        doReturn(1_000_000L).when(mIrs).getConsumptionLimitLocked();
Loading