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

Commit 891e399b authored by Kweku Adams's avatar Kweku Adams
Browse files

Add basic TARE StateController.

Add a StateController that will handle the TARE wealth constraint for
jobs. This adds the basic implementation but doesn't fully hook up the
controller with the whole JobScheduler flow.

Bug: 158300259
Test: atest frameworks/base/services/tests/mockingservicestests/src/com/android/server/job
Test: atest frameworks/base/services/tests/servicestests/src/com/android/server/job
Test: atest CtsJobSchedulerTestCases
Change-Id: I5a288546f0ec215a394edf821318b00d4f8db63d
parent 892abaf4
Loading
Loading
Loading
Loading
+54 −1
Original line number Diff line number Diff line
@@ -89,6 +89,7 @@ public final class JobStatus {
    static final int CONSTRAINT_TIMING_DELAY = 1<<31;
    static final int CONSTRAINT_DEADLINE = 1<<30;
    static final int CONSTRAINT_CONNECTIVITY = 1 << 28;
    static final int CONSTRAINT_TARE_WEALTH = 1 << 27; // Implicit constraint
    static final int CONSTRAINT_CONTENT_TRIGGER = 1<<26;
    static final int CONSTRAINT_DEVICE_NOT_DOZING = 1 << 25; // Implicit constraint
    static final int CONSTRAINT_WITHIN_QUOTA = 1 << 24;      // Implicit constraint
@@ -146,6 +147,7 @@ public final class JobStatus {
    private static final int STATSD_CONSTRAINTS_TO_LOG = CONSTRAINT_CONTENT_TRIGGER
            | CONSTRAINT_DEADLINE
            | CONSTRAINT_IDLE
            | CONSTRAINT_TARE_WEALTH
            | CONSTRAINT_TIMING_DELAY
            | CONSTRAINT_WITHIN_QUOTA;

@@ -395,6 +397,10 @@ public final class JobStatus {
     * Whether or not this job is approved to be treated as expedited per quota policy.
     */
    private boolean mExpeditedQuotaApproved;
    /**
     * Whether or not this job is approved to be treated as expedited per TARE policy.
     */
    private boolean mExpeditedTareApproved;

    /////// Booleans that track if a job is ready to run. They should be updated whenever dependent
    /////// states change.
@@ -421,6 +427,9 @@ public final class JobStatus {
    /** The job is within its quota based on its standby bucket. */
    private boolean mReadyWithinQuota;

    /** The job has enough credits to run based on TARE. */
    private boolean mReadyTareWealth;

    /** The job's dynamic requirements have been satisfied. */
    private boolean mReadyDynamicSatisfied;

@@ -1230,6 +1239,16 @@ public final class JobStatus {
        return false;
    }

    /** @return true if the constraint was changed, false otherwise. */
    boolean setTareWealthConstraintSatisfied(final long nowElapsed, boolean state) {
        if (setConstraintSatisfied(CONSTRAINT_TARE_WEALTH, nowElapsed, state)) {
            // The constraint was changed. Update the ready flag.
            mReadyTareWealth = state;
            return true;
        }
        return false;
    }

    /**
     * Sets whether or not this job is approved to be treated as an expedited job based on quota
     * policy.
@@ -1245,6 +1264,21 @@ public final class JobStatus {
        return true;
    }

    /**
     * Sets whether or not this job is approved to be treated as an expedited job based on TARE
     * policy.
     *
     * @return true if the approval bit was changed, false otherwise.
     */
    boolean setExpeditedJobTareApproved(final long nowElapsed, boolean state) {
        if (mExpeditedTareApproved == state) {
            return false;
        }
        mExpeditedTareApproved = state;
        updateExpeditedDependencies();
        return true;
    }

    private void updateExpeditedDependencies() {
        // DeviceIdleJobsController currently only tracks jobs with the WILL_BE_FOREGROUND flag.
        // Making it also track requested-expedited jobs would add unnecessary hops since the
@@ -1353,6 +1387,7 @@ public final class JobStatus {
            case CONSTRAINT_DEVICE_NOT_DOZING:
                return JobParameters.STOP_REASON_DEVICE_STATE;

            case CONSTRAINT_TARE_WEALTH:
            case CONSTRAINT_WITHIN_QUOTA:
                return JobParameters.STOP_REASON_QUOTA;

@@ -1405,6 +1440,11 @@ public final class JobStatus {
            Slog.wtf(TAG, "Tried to set quota as a dynamic constraint");
            constraints &= ~CONSTRAINT_WITHIN_QUOTA;
        }
        if ((constraints & CONSTRAINT_TARE_WEALTH) != 0) {
            // Quota should never be used as a dynamic constraint.
            Slog.wtf(TAG, "Tried to set TARE as a dynamic constraint");
            constraints &= ~CONSTRAINT_TARE_WEALTH;
        }

        // Connectivity and content trigger are special since they're only valid to add if the
        // job has requested network or specific content URIs. Adding these constraints to jobs
@@ -1472,6 +1512,10 @@ public final class JobStatus {
                oldValue = mReadyNotDozing;
                mReadyNotDozing = value;
                break;
            case CONSTRAINT_TARE_WEALTH:
                oldValue = mReadyTareWealth;
                mReadyTareWealth = value;
                break;
            case CONSTRAINT_WITHIN_QUOTA:
                oldValue = mReadyWithinQuota;
                mReadyWithinQuota = value;
@@ -1500,6 +1544,9 @@ public final class JobStatus {
            case CONSTRAINT_DEVICE_NOT_DOZING:
                mReadyNotDozing = oldValue;
                break;
            case CONSTRAINT_TARE_WEALTH:
                mReadyTareWealth = oldValue;
                break;
            case CONSTRAINT_WITHIN_QUOTA:
                mReadyWithinQuota = oldValue;
                break;
@@ -1727,6 +1774,9 @@ public final class JobStatus {
        if ((constraints&CONSTRAINT_BACKGROUND_NOT_RESTRICTED) != 0) {
            pw.print(" BACKGROUND_NOT_RESTRICTED");
        }
        if ((constraints & CONSTRAINT_TARE_WEALTH) != 0) {
            pw.print(" TARE_WEALTH");
        }
        if ((constraints & CONSTRAINT_WITHIN_QUOTA) != 0) {
            pw.print(" WITHIN_QUOTA");
        }
@@ -1991,7 +2041,8 @@ public final class JobStatus {
            pw.println();
            pw.print("Unsatisfied constraints:");
            dumpConstraints(pw,
                    ((requiredConstraints | CONSTRAINT_WITHIN_QUOTA) & ~satisfiedConstraints));
                    ((requiredConstraints | CONSTRAINT_WITHIN_QUOTA | CONSTRAINT_TARE_WEALTH)
                            & ~satisfiedConstraints));
            pw.println();

            pw.println("Constraint history:");
@@ -2050,6 +2101,8 @@ public final class JobStatus {
        if ((getFlags() & JobInfo.FLAG_EXPEDITED) != 0) {
            pw.print("expeditedQuotaApproved: ");
            pw.print(mExpeditedQuotaApproved);
            pw.print(" expeditedTareApproved: ");
            pw.print(mExpeditedTareApproved);
            pw.print(" (started as EJ: ");
            pw.print(startedAsExpeditedJob);
            pw.println(")");
+2 −2
Original line number Diff line number Diff line
@@ -160,8 +160,8 @@ public abstract class StateController {

    public abstract void dumpControllerStateLocked(IndentingPrintWriter pw,
            Predicate<JobStatus> predicate);
    public abstract void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId,
            Predicate<JobStatus> predicate);
    public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId,
            Predicate<JobStatus> predicate) {}

    /** Dump any internal constants the Controller may have. */
    public void dumpConstants(IndentingPrintWriter pw) {
+418 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.job.controllers;

import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;

import android.annotation.NonNull;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArrayMap;

import com.android.internal.annotations.GuardedBy;
import com.android.server.LocalServices;
import com.android.server.job.JobSchedulerService;
import com.android.server.tare.EconomyManagerInternal;
import com.android.server.tare.EconomyManagerInternal.ActionBill;
import com.android.server.tare.JobSchedulerEconomicPolicy;

import java.util.List;
import java.util.function.Predicate;

/**
 * Controller that interfaces with Tare ({@link EconomyManagerInternal} and manages each job's
 * ability to run per TARE policies.
 *
 * @see JobSchedulerEconomicPolicy
 */
public class TareController extends StateController {
    private static final String TAG = "JobScheduler.TARE";
    private static final boolean DEBUG = JobSchedulerService.DEBUG
            || Log.isLoggable(TAG, Log.DEBUG);

    /**
     * Bill to use while we're waiting to start a job. If a job isn't running yet, don't consider it
     * eligible to run unless it can pay for a job start and at least some period of execution time.
     */
    private static final ActionBill BILL_JOB_START_DEFAULT =
            new ActionBill(List.of(
                    new EconomyManagerInternal.AnticipatedAction(
                            JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_START, 1, 0),
                    new EconomyManagerInternal.AnticipatedAction(
                            JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING, 0, 30_000L)
            ));

    /**
     * Bill to use when a default is currently running. We want to track and make sure the app can
     * continue to pay for 1 more second of execution time. We stop the job when the app can no
     * longer pay for that time.
     */
    private static final ActionBill BILL_JOB_RUNNING_DEFAULT =
            new ActionBill(List.of(
                    new EconomyManagerInternal.AnticipatedAction(
                            JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING, 0, 1_000L)
            ));

    /**
     * Bill to use while we're waiting to start a job. If a job isn't running yet, don't consider it
     * eligible to run unless it can pay for a job start and at least some period of execution time.
     */
    private static final ActionBill BILL_JOB_START_EXPEDITED =
            new ActionBill(List.of(
                    new EconomyManagerInternal.AnticipatedAction(
                            JobSchedulerEconomicPolicy.ACTION_JOB_MAX_START, 1, 0),
                    new EconomyManagerInternal.AnticipatedAction(
                            JobSchedulerEconomicPolicy.ACTION_JOB_MAX_RUNNING, 0, 30_000L)
            ));

    /**
     * Bill to use when an EJ is currently running (as an EJ). We want to track and make sure the
     * app can continue to pay for 1 more second of execution time. We stop the job when the app can
     * no longer pay for that time.
     */
    private static final ActionBill BILL_JOB_RUNNING_EXPEDITED =
            new ActionBill(List.of(
                    new EconomyManagerInternal.AnticipatedAction(
                            JobSchedulerEconomicPolicy.ACTION_JOB_MAX_RUNNING, 0, 1_000L)
            ));

    private final EconomyManagerInternal mEconomyManagerInternal;

    private final BackgroundJobsController mBackgroundJobsController;
    private final ConnectivityController mConnectivityController;

    /**
     * Local cache of the ability of each userId-pkg to afford the various bills we're tracking for
     * them.
     */
    @GuardedBy("mLock")
    private final SparseArrayMap<String, ArrayMap<ActionBill, Boolean>> mAffordabilityCache =
            new SparseArrayMap<>();

    /**
     * List of all tracked jobs. Out SparseArrayMap is userId-sourcePkg. The inner mapping is the
     * anticipated actions and all the jobs that are applicable to them.
     */
    @GuardedBy("mLock")
    private final SparseArrayMap<String, ArrayMap<ActionBill, ArraySet<JobStatus>>>
            mRegisteredBillsAndJobs = new SparseArrayMap<>();

    private final EconomyManagerInternal.AffordabilityChangeListener mAffordabilityChangeListener =
            (userId, pkgName, bill, canAfford) -> {
                final long nowElapsed = sElapsedRealtimeClock.millis();
                if (DEBUG) {
                    Slog.d(TAG,
                            userId + ":" + pkgName + " affordability for " + getBillName(bill)
                                    + " changed to " + canAfford);
                }
                synchronized (mLock) {
                    ArrayMap<ActionBill, Boolean> actionAffordability =
                            mAffordabilityCache.get(userId, pkgName);
                    if (actionAffordability == null) {
                        actionAffordability = new ArrayMap<>();
                        mAffordabilityCache.add(userId, pkgName, actionAffordability);
                    }
                    actionAffordability.put(bill, canAfford);

                    final ArrayMap<ActionBill, ArraySet<JobStatus>> billToJobMap =
                            mRegisteredBillsAndJobs.get(userId, pkgName);
                    if (billToJobMap != null) {
                        final ArraySet<JobStatus> jobs = billToJobMap.get(bill);
                        if (jobs != null) {
                            final ArraySet<JobStatus> changedJobs = new ArraySet<>();
                            for (int i = 0; i < jobs.size(); ++i) {
                                final JobStatus job = jobs.valueAt(i);
                                // Use hasEnoughWealth if canAfford is false in case the job has
                                // other bills it can depend on (eg. EJs being demoted to
                                // regular jobs).
                                if (job.setTareWealthConstraintSatisfied(nowElapsed,
                                        canAfford || hasEnoughWealthLocked(job))) {
                                    changedJobs.add(job);
                                }
                                if (job.isRequestedExpeditedJob()
                                        && setExpeditedTareApproved(job, nowElapsed,
                                        canAffordExpeditedBillLocked(job))) {
                                    changedJobs.add(job);
                                }
                            }
                            if (changedJobs.size() > 0) {
                                mStateChangedListener.onControllerStateChanged(changedJobs);
                            }
                        }
                    }
                }
            };

    @GuardedBy("mLock")
    private boolean mIsEnabled;

    public TareController(JobSchedulerService service,
            @NonNull BackgroundJobsController backgroundJobsController,
            @NonNull ConnectivityController connectivityController) {
        super(service);
        mBackgroundJobsController = backgroundJobsController;
        mConnectivityController = connectivityController;
        mEconomyManagerInternal = LocalServices.getService(EconomyManagerInternal.class);
    }

    @Override
    public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) {
        final long nowElapsed = sElapsedRealtimeClock.millis();
        jobStatus.setTareWealthConstraintSatisfied(nowElapsed, hasEnoughWealthLocked(jobStatus));
        setExpeditedTareApproved(jobStatus, nowElapsed,
                jobStatus.isRequestedExpeditedJob() && canAffordExpeditedBillLocked(jobStatus));

        final ArraySet<ActionBill> bills = getPossibleStartBills(jobStatus);
        for (int i = 0; i < bills.size(); ++i) {
            addJobToBillList(jobStatus, bills.valueAt(i));
        }
    }

    @Override
    public void prepareForExecutionLocked(JobStatus jobStatus) {
        final int userId = jobStatus.getSourceUserId();
        final String pkgName = jobStatus.getSourcePackageName();
        ArrayMap<ActionBill, ArraySet<JobStatus>> billToJobMap =
                mRegisteredBillsAndJobs.get(userId, pkgName);
        if (billToJobMap == null) {
            Slog.e(TAG, "Job is being prepared but doesn't have a pre-existing billToJobMap");
        } else {
            for (int i = 0; i < billToJobMap.size(); ++i) {
                removeJobFromBillList(jobStatus, billToJobMap.keyAt(i));
            }
        }
        if (jobStatus.shouldTreatAsExpeditedJob()) {
            addJobToBillList(jobStatus, BILL_JOB_RUNNING_EXPEDITED);
        }
        addJobToBillList(jobStatus, BILL_JOB_RUNNING_DEFAULT);
    }

    @Override
    public void unprepareFromExecutionLocked(JobStatus jobStatus) {
        final int userId = jobStatus.getSourceUserId();
        final String pkgName = jobStatus.getSourcePackageName();
        final ArraySet<ActionBill> bills = getPossibleStartBills(jobStatus);
        ArrayMap<ActionBill, ArraySet<JobStatus>> billToJobMap =
                mRegisteredBillsAndJobs.get(userId, pkgName);
        if (billToJobMap == null) {
            Slog.e(TAG, "Job was just unprepared but didn't have a pre-existing billToJobMap");
        } else {
            for (int i = 0; i < billToJobMap.size(); ++i) {
                removeJobFromBillList(jobStatus, billToJobMap.keyAt(i));
            }
        }
        for (int i = 0; i < bills.size(); ++i) {
            addJobToBillList(jobStatus, bills.valueAt(i));
        }
    }

    @Override
    public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
            boolean forUpdate) {
        final int userId = jobStatus.getSourceUserId();
        final String pkgName = jobStatus.getSourcePackageName();
        ArrayMap<ActionBill, ArraySet<JobStatus>> billToJobMap =
                mRegisteredBillsAndJobs.get(userId, pkgName);
        if (billToJobMap != null) {
            for (int i = 0; i < billToJobMap.size(); ++i) {
                removeJobFromBillList(jobStatus, billToJobMap.keyAt(i));
            }
        }
    }

    @GuardedBy("mLock")
    public boolean canScheduleEJ(@NonNull JobStatus jobStatus) {
        if (!mIsEnabled) {
            return true;
        }
        return canAffordBillLocked(jobStatus, BILL_JOB_START_EXPEDITED);
    }

    @GuardedBy("mLock")
    private void addJobToBillList(@NonNull JobStatus jobStatus, @NonNull ActionBill bill) {
        final int userId = jobStatus.getSourceUserId();
        final String pkgName = jobStatus.getSourcePackageName();
        ArrayMap<ActionBill, ArraySet<JobStatus>> billToJobMap =
                mRegisteredBillsAndJobs.get(userId, pkgName);
        if (billToJobMap == null) {
            billToJobMap = new ArrayMap<>();
            mRegisteredBillsAndJobs.add(userId, pkgName, billToJobMap);
        }
        ArraySet<JobStatus> jobs = billToJobMap.get(bill);
        if (jobs == null) {
            jobs = new ArraySet<>();
            billToJobMap.put(bill, jobs);
        }
        if (jobs.add(jobStatus)) {
            mEconomyManagerInternal.registerAffordabilityChangeListener(userId, pkgName,
                    mAffordabilityChangeListener, bill);
        }
    }

    @GuardedBy("mLock")
    private void removeJobFromBillList(@NonNull JobStatus jobStatus, @NonNull ActionBill bill) {
        final int userId = jobStatus.getSourceUserId();
        final String pkgName = jobStatus.getSourcePackageName();
        final ArrayMap<ActionBill, ArraySet<JobStatus>> billToJobMap =
                mRegisteredBillsAndJobs.get(userId, pkgName);
        if (billToJobMap != null) {
            final ArraySet<JobStatus> jobs = billToJobMap.get(bill);
            if (jobs == null || (jobs.remove(jobStatus) && jobs.size() == 0)) {
                mEconomyManagerInternal.unregisterAffordabilityChangeListener(
                        userId, pkgName, mAffordabilityChangeListener, bill);
                // Remove the cached value so we don't accidentally use it when the app
                // schedules a new job.
                final ArrayMap<ActionBill, Boolean> actionAffordability =
                        mAffordabilityCache.get(userId, pkgName);
                if (actionAffordability != null) {
                    actionAffordability.remove(bill);
                }
            }
        }
    }

    @NonNull
    private ArraySet<ActionBill> getPossibleStartBills(JobStatus jobStatus) {
        // TODO: factor in network cost when available
        final ArraySet<ActionBill> bills = new ArraySet<>();
        if (jobStatus.isRequestedExpeditedJob()) {
            bills.add(BILL_JOB_START_EXPEDITED);
        }
        bills.add(BILL_JOB_START_DEFAULT);
        return bills;
    }

    @GuardedBy("mLock")
    private boolean canAffordBillLocked(@NonNull JobStatus jobStatus, @NonNull ActionBill bill) {
        if (!mIsEnabled) {
            return true;
        }
        final int userId = jobStatus.getSourceUserId();
        final String pkgName = jobStatus.getSourcePackageName();
        ArrayMap<ActionBill, Boolean> actionAffordability =
                mAffordabilityCache.get(userId, pkgName);
        if (actionAffordability == null) {
            actionAffordability = new ArrayMap<>();
            mAffordabilityCache.add(userId, pkgName, actionAffordability);
        }

        if (actionAffordability.containsKey(bill)) {
            return actionAffordability.get(bill);
        }

        final boolean canAfford = mEconomyManagerInternal.canPayFor(userId, pkgName, bill);
        actionAffordability.put(bill, canAfford);
        return canAfford;
    }

    @GuardedBy("mLock")
    private boolean canAffordExpeditedBillLocked(@NonNull JobStatus jobStatus) {
        if (!mIsEnabled) {
            return true;
        }
        if (mService.isCurrentlyRunningLocked(jobStatus)) {
            return canAffordBillLocked(jobStatus, BILL_JOB_RUNNING_EXPEDITED);
        }

        return canAffordBillLocked(jobStatus, BILL_JOB_START_EXPEDITED);
    }

    @GuardedBy("mLock")
    private boolean hasEnoughWealthLocked(@NonNull JobStatus jobStatus) {
        if (!mIsEnabled) {
            return true;
        }
        if (mService.isCurrentlyRunningLocked(jobStatus)) {
            if (jobStatus.isRequestedExpeditedJob()) {
                return canAffordBillLocked(jobStatus, BILL_JOB_RUNNING_EXPEDITED)
                        || canAffordBillLocked(jobStatus, BILL_JOB_RUNNING_DEFAULT);
            }
            return canAffordBillLocked(jobStatus, BILL_JOB_RUNNING_DEFAULT);
        }

        if (jobStatus.isRequestedExpeditedJob()
                && canAffordBillLocked(jobStatus, BILL_JOB_START_EXPEDITED)) {
            return true;
        }

        return canAffordBillLocked(jobStatus, BILL_JOB_START_DEFAULT);
    }

    /**
     * If the satisfaction changes, this will tell connectivity & background jobs controller to
     * also re-evaluate their state.
     */
    private boolean setExpeditedTareApproved(@NonNull JobStatus jobStatus, long nowElapsed,
            boolean isApproved) {
        if (jobStatus.setExpeditedJobTareApproved(nowElapsed, isApproved)) {
            mBackgroundJobsController.evaluateStateLocked(jobStatus);
            mConnectivityController.evaluateStateLocked(jobStatus);
            if (isApproved && jobStatus.isReady()) {
                mStateChangedListener.onRunJobNow(jobStatus);
            }
            return true;
        }
        return false;
    }

    @NonNull
    private String getBillName(@NonNull ActionBill bill) {
        if (bill.equals(BILL_JOB_START_EXPEDITED)) {
            return "EJ_START_BILL";
        }
        if (bill.equals(BILL_JOB_RUNNING_EXPEDITED)) {
            return "EJ_RUNNING_BILL";
        }
        if (bill.equals(BILL_JOB_START_DEFAULT)) {
            return "DEFAULT_START_BILL";
        }
        if (bill.equals(BILL_JOB_RUNNING_DEFAULT)) {
            return "DEFAULT_RUNNING_BILL";
        }
        return "UNKNOWN_BILL (" + bill.toString() + ")";
    }

    @Override
    public void dumpControllerStateLocked(IndentingPrintWriter pw, Predicate<JobStatus> predicate) {
        pw.print("Is enabled: ");
        pw.println(mIsEnabled);

        pw.println("Affordability cache:");
        pw.increaseIndent();
        mAffordabilityCache.forEach((userId, pkgName, billMap) -> {
            final int numBills = billMap.size();
            if (numBills > 0) {
                pw.print(userId);
                pw.print(":");
                pw.print(pkgName);
                pw.println(":");

                pw.increaseIndent();
                for (int i = 0; i < numBills; ++i) {
                    pw.print(getBillName(billMap.keyAt(i)));
                    pw.print(": ");
                    pw.println(billMap.valueAt(i));
                }
                pw.decreaseIndent();
            }
        });
        pw.decreaseIndent();
    }
}