SurfaceFlinger稳定性01-Layer泄漏问题日志增强方案

作者时间描述
贾勇强2024.04.15system_server、sf范围添加dump信息和相关日志

文档地址 https://xiaomi.f.mioffice.cn/docx/doxk47tUJYR69gPB3qSzlDPe9hg

Layer泄漏潜在影响因素较多、涉及业务模块多,在项目交付过程中出现问题时,经常很难进一步定位具体原因。

因此,这里针对Layer泄漏场景添加些必要信息,帮忙研发在遇到问题时能快速定位问题。

导致Layer泄漏的主要原因有:

  1. SurfaceControl操作不合理。

​ ◦ 业务侧在使用完毕SurfaceControl后,没有执行SurfaceControl.release()进行释放;

​ ◦ 业务侧在释放时,如果存在父SurfaceControl,没有执行SurfaceControl.reparent(null);

​ ◦ 业务侧创建了多份SurfaceControl副本,且没有对所有副本都执行以上操作。

  1. LayerHandle操作不合理。

​ ◦ SurfaceControl未析构,持有LayerHandle引用;

​ ◦ ComposerState未析构,持有LayerHandle引用。

  1. Layer操作不合理。

​ ◦ 释放时没有将Parent Layer置空;

​ ◦ 仅对Parent Layer进行释放,未释放子Layer;

一、确认业务侧是否释放SurfaceControl

surfaceflinger进程内的信息无法明确确认业务侧SurfaceControl是否释放,比如当LayerHandle未析构时,并不能直接说明业务侧SurfaceControl中还持有LayerHandle,sf进程内部有可能也存在对LayerHandle的间接引用(比如通过TransactionState持有)。

同时,SurfaceControl和Layer并非一对一的关系,如果一个SurfaceControl通过copy另一个SurfaceControl创建,其共享同一个Layer,并且只有他们全部释放后,才会析构LayerHandle(如果sf侧没有LayerHandle引用)。

1.1、解决方案:业务进程添加关键日志信息和dump信息

原生代码中已实现对SurfaceControl泄漏相关逻辑,这里直接对原生逻辑进行应用。

在SurfaceControl创建时,通过SurfaceControlRegistry类对每个进程会进行SC的统一,我们可以利用这个类进行日志和dump信息添加。

在创建SurfaceControl时,将其添加到SurfaceControlRegistry中,当该进程内SurfaceControl个数超过阈值时,输出相关日志,代码逻辑如下:

// frameworks/base/core/java/android/view/SurfaceControl.java​
 
// 创建SurfaceControl时,会将其添加到列表中​
private void addToRegistry() {
    final SurfaceControlRegistry registry = SurfaceControlRegistry.getProcessInstance();
    if (registry != null) {
        registry.add(this);
    }
}
    
// frameworks/base/core/java/android/view/SurfaceControlRegistry.java
// 添加过程中,当超过设置阈值时,会输出日志
void add(SurfaceControl sc) {
    synchronized (sLock) {
        mSurfaceControls.put(sc, SystemClock.elapsedRealtime());
        if (!mHasReportedExceedingMaxThreshold
                && mSurfaceControls.size() >= mMaxLayersReportingThreshold) {
            PrintWriter pw = new PrintWriter(System.out, true /* autoFlush */);
            mReporter.onMaxLayersExceeded(mSurfaceControls, DUMP_LIMIT, pw);
        }
    }
}
 
// dump时,输出当前存留未释放SurfaceControl
public static void dump(int limit, boolean runGc, PrintWriter pw) {​
    ......
    synchronized (sLock) {
        if (sProcessRegistry != null) {
            sDefaultReporter.onMaxLayersExceeded(sProcessRegistry.mSurfaceControls, limit, pw);
        }
    }
}
 

1.1.1、system_server接入

system_server中接入SurfaceControlRegistry分两部分:

  1. WMS启动过程中初始化SurfaceControlRegistry实例;

  2. 执行gfxinfo、window dumpsys输出信息中添加当前SurfaceControl输出;

1.1.1.1、初始化SurfaceControlRegistry

在WindowMangerService启动时,可以通过SurfaceControlRegistry对system_server进程内的SurfaceControl进行统计和输出。这里添加相关代码如下:

WMS启动过程中,创建system_server进程内SurfaceControlRegistry实例:

// frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java​
    private WindowManagerService(Context context, InputManagerService inputManager,​
            boolean showBootMsgs, WindowManagerPolicy policy, ActivityTaskManagerService atm,​
            DisplayWindowSettingsProvider displayWindowSettingsProvider,​
            Supplier<SurfaceControl.Transaction> transactionFactory,​
            Function<SurfaceSession, SurfaceControl.Builder> surfaceControlFactory) {​
        ........​
        // MIUI ADD: SF_LogEnhance​
        // Used for debugging layer leaks.​
        // 创建SurfaceControlRegistry实例​
        SurfaceControlRegistry.createProcessInstance(mContext);​
        // END SF_LogEnhance​
    }

http://minio.898311.xyz:8900/blogimg/17248133117449.png

1.1.1.2、dumpsys输出添加SurfaceControl信息

对当前SurfaceControl信息输出,在dump gfxinfo和window中分别进行了添加,以支持其他业务进程的信息输出。

• wms中,携带参数执行dump时可输出system_server侧SurfaceControl信息:

adb shell dumpsys window surfacecontrol

gfxinfo中,可以为所有进程提供SurfaceControl输出

1.1.2、SystemUI/Lancher/壁纸接入

初步针对当前Layer泄漏/DMA-Buf泄漏高发场景systemui、Lancher、壁纸,接入该方案。

业务侧接入方式:进程启动后注册一个SurfaceControlRegistry实例即可,如:

import android.view.SurfaceControlRegistry;
 
public Method() {
    // 创建进程内单例SurfaceControlRegistry对象
    SurfaceControlRegistry.createProcessInstance(mApplicationContext);​
}

1.2、log信息输出

adb logcat | grep -Ei "I System.out:|SurfaceControlRegistry"

1.3、dump 信息输出

dump命令:

adb shell dumpsys window surfacecontrol  // 仅输出system_server内SC
# 或
adb shell dumpsys gfxinfo system

业务侧以SystemUI为例,dump及输出结果如下:

adb shell dumpsys gfxinfo com.android.systemui

https://gerrit.pt.mioffice.cn/c/platform/frameworks/base/+/3940078

📌

当问题发生时,如果system_server或相关进程侧无泄漏信息,但sf侧中泄漏Layer显示LayerHandle未释放,即可排除SurfaceControl.release()方法未调用的可能性。

二、SurfaceControl已经释放,但LayerHandle未释放

如果所有引用LayerHandle的SurfaceControl已确认释放,但LayerHandle并未释放,基本可以确定LayerHandle在surfaceflinger进程内部存在引用。

系统规定LayerHandle只能作为暴露给客户端用于操作Layer的”句柄”,不能在surfaceflinger内部出现任何引用它的地方,不过却存在间接使用它的地方——TransactionState。

所有客户端的Transaction发起提交后,会以TranactionState的方式传递给surfaceflinger中,TranactionState中会携带有LayerHandle。

surfaceflinger中收到来自客户端传递的Transaction后,会把这些TranactionState入对,并且在下一帧VSNC对所有队列中的Tranaction进行处理,并在应用完成后清空队列。

问题在于,如果TranactionState队列一直没有清空,那么即便SurfaceControl释放,LayerHandle也无法进行析构,这时将导致Layer泄漏。

TranactionState队列堆积原因一般有以下几种:

  1. Buffer Barrier未移除;

  2. Buffer desiredPresentTime未到达;

  3. Buffer Fence信号未收到。

后两种情况很少见,重点是第一种情况,一旦给Barrier Buffer设置的frameNumber出现异常,将直接导致相同ApplyToken的Transaction队列中的所有Transaction堆积。

2.1 解决方案:dump Transaction队列信息、添加针对Barrier Buffer相关日志

https://gerrit.pt.mioffice.cn/c/platform/frameworks/native/+/3935615

在TransactionHandler中,添加dump信息:

// frameworks/native/services/surfaceflinger/FrontEnd/TransactionHandler.cpp​
// MIUI ADD: SF_LogEnhance​
std::string TransactionHandler::dump() {​
    std::string result;​
    const nsecs_t now = systemTime();​
    base::StringAppendF(&result, "\nPending transaction queue state(size=%d):\n",​
                        mPendingTransactionQueues.size());​
    base::StringAppendF(&result, "  Total transactions: %d\n",​
                        static_cast<int>(mPendingTransactionCount.load()));​
    auto it = mPendingTransactionQueues.begin();​
    while (it != mPendingTransactionQueues.end()) {​
        auto& [token, queue] = *it;​
        base::StringAppendF(&result, "  ApplyToken: %p, size: %d", token.get(), queue.size());​
        // We only dump the first one transaction from queues corresponding to each token,​
        // it's enough for debugging barrier buffer.​
        if (!queue.empty()) {​
            auto& transactionState = queue.front();​
            base::StringAppendF(&result,​
                                dumpTransactionState(transactionState, now).c_str());​
        }​
        ++it;​
    }​
    return result;​
}​

std::string TransactionHandler::dumpTransactionState(const TransactionState& ts,​
                                                     const nsecs_t& now) {​
    std::string result;​
    int maxLayerCount = 10;​
    bool startDump = false;​
    result.append("\n    ");​
    base::StringAppendF(&result,​
                        "TransactionState: id=%" PRIu64 ", originPid=%d"
                        ", desiredPresentTime=%" PRId64 "(%.2fms ago), postTime=%" PRId64​
                        "(%.2fms ago)"
                        ", deferAnimation=%d, flags: %" PRIu32,​
                        ts.id, ts.originPid, ts.desiredPresentTime,​
                        (now - ts.desiredPresentTime) / 1e6f, ts.postTime,​
                        (now - ts.postTime) / 1e6f, ts.deferAnimation, ts.flags);​
    result.append("\n      ");​
    base::StringAppendF(&result, "Resolved states(size=%d): ", ts.states.size());​
    for (auto& resolvedState : ts.states) {​
        sp<Layer> layer = LayerHandle::getLayer(resolvedState.state.surface);​
        std::string layerName =
                (layer) ? layer->getDebugName() : std::to_string(resolvedState.layerId);​
        bool hasVaildBuffer = resolvedState.state.hasValidBuffer();​
        bool hasBarrier = hasVaildBuffer && resolvedState.state.bufferData->hasBarrier;​
        // Only dump layer with buffer barrier and its last 10 layers.​
        startDump |= hasBarrier;​
        if (startDump) {​
            result.append("\n        ");​
            base::StringAppendF(&result, "  Layer: %s, layerId: %" PRIu32 ", parentId: %" PRIu32,​
                                layerName.c_str(), resolvedState.layerId, resolvedState.parentId);​
            if (hasBarrier) {​
                result.append("\n                 ");​
                base::StringAppendF(&result, "hasBuffer: %s, hasBufferBarrier: %s",​
                                    hasVaildBuffer ? "true" : "false",​
                                    hasBarrier ? "true" : "false");​
                result.append("\n                 ");​
                base::StringAppendF(&result,​
                                    "drawing barrierFrameNumber: %" PRIu64​
                                    ", pending barrierFrameNumber: %" PRIu64,​
                                    layer->getDrawingState().barrierFrameNumber,​
                                    resolvedState.state.bufferData->barrierFrameNumber);​
            }​
            --maxLayerCount;​
        }​
        if (maxLayerCount < 0) {​
            break;​
        }​
    }​
    result.append("\n");​
    return result;​
}​

为避免输出信息过多,对输出内容进行精简:

  1. 只输出TransactionState队列中的队头TransactionState,它是阻塞之源;

  2. 针对每个TransactionState中的Layer,从携带BufferBarrier的ComposerState开始向后最多输出10个ComposerState信息。

2.1.1、dump信息输出

出现问题时,输出结果如下

adb shell dumpsys SurfaceFlinger

1: 以ApplyToken为Key键、TransactionState列表为值的映射表元素个数;

2: 所有TransactionState的个数;

3: TransactionState列表中的元素个数;

4: ResolvedState列表中对应操作Layer的个数;

5: GraphicBuffer设置了barrier的具体Layer。

三、OffScreenLayer泄漏

OffScreenLayer泄漏,一般有以下几类场景:

  1. LayerHandle已释放,但Layer没有析构时,会加入OffScreenLayer列表中无法移除;

  2. LayerHandle未释放,但其Parent Layer释放后,该Layer会加入OffScreenLayer列表无法移除;

  3. LayerHandle未释放,对其进行reparent时,将其reparent到一个空Parent或OffScreenLayer上时,该Layer会加入OffScreenLayer列表无法移除;

3.1、解决方案: OffscreenLayer dump信息中添加parent信息

// frameworks/native/services/surfaceflinger/Layer.cpp​

void Layer::dumpOffscreenDebugInfo(std::string& result) const {​
    std::string hasBuffer = hasBufferOrSidebandStream() ? " (contains buffer)" : "";​
    // MIUI MOD: SF_LogEnhance​
    // StringAppendF(&result, "Layer %s%s pid:%d uid:%d%s\n", getName().c_str(), hasBuffer.c_str(),​
    //               mOwnerPid, mOwnerUid, isHandleAlive() ? " handleAlive" : "");​
    StringAppendF(&result, "Layer %s%s pid:%d uid:%d%s%s\n", getName().c_str(), hasBuffer.c_str(),​
                  mOwnerPid, mOwnerUid, isHandleAlive() ? " handleAlive" : "",​
                  getParent() ? std::string(" parent(")​
                                        .append(getParent()->getDebugName())​
                                        .append(")")​
                                        .c_str()​
                              : "");​
    // END SF_LogEnhance​
}

3.2、dump信息输出

执行adb shell dumpsys SurfaceFlinger后,输出结果如下:

仅有handleAlive:说明LayerHandle没有释放且没有Parent,可确认是reparent(null)导致;

  • handleAlive + parent都存在: 说明LayerHandle没有释放且存在Parent,可确认是reparent(OffscreenLayer)导致;
  • 仅有parent:说明LayerHandle已经释放,但其Parent没有释放;

四、代码提交

surfaceflinger侧改动:

https://gerrit.pt.mioffice.cn/c/platform/frameworks/native/+/3935615

system_server侧改动:

https://gerrit.pt.mioffice.cn/c/platform/frameworks/base/+/3940078