mcpField
T
taskpool-dependency-analysis

taskpool-dependency-analysis

github.com/openharmonyinsight开发3541 次安装11 周前更新
代码仓库

关于 taskpool-dependency-analysis

taskpool-dependency-analysis 用于诊断静态 ArkTS 运行时中的任务依赖问题,指导开发者构建依赖图、检查循环与 API 调用顺序、追踪通知和取消传播并提出修复建议。

主要功能

  1. 01根据代码构建任务依赖图
  2. 02检测循环和非法任务类型边
  3. 03追踪通知与任务激活路径
  4. 04分析取消传播
  5. 05提供结构化诊断与修复建议

适用场景

  1. 01排查 taskpool 任务未执行的问题
  2. 02诊断循环或缺失的依赖关系
  3. 03发现无效的依赖 API 调用顺序
  4. 04追踪任务间的取消传播故障

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/openharmonyinsight/openharmony-skills --skill taskpool-dependency-analysis

安装后在 Claude Code 中输入/taskpool-dependency-analysis确认已加载。

附带文件evals/evals.jsonreferences/dependency_reference.md

详细介绍

任务池依赖分析技能

此技能可帮助你系统地诊断和修复静态 ArkTS 运行时中的任务池依赖问题。它将引导你从用户代码构建依赖图、检测循环、识别非法 API 调用顺序、追踪通知/激活路径,并提供修复建议。

此技能的用途

任务池依赖并非“只是检测死锁”,而是一套定制的任务依赖模型,具有自身的状态机、通知机制、任务类型限制和取消传播规则。依赖于另一任务的任务会通过 waitForDependencies() 阻塞,直到其 taskDependenciesCount 归零;前置任务完成后,会调用 notifyDependencies(),递减计数器,并通过 tryActivatePendingDependencyTask 将已就绪的依赖任务重新加入队列。如果此链条中的任一步骤出现问题,依赖任务就可能悄无声息地挂起或永不执行,而根本原因通常并非表面症状出现的位置。

源代码绑定

使用此技能时,请阅读以下源文件,以权威实现为准:

  • 静态标准库:runtime_core/static_core/plugins/ets/stdlib/std/concurrency/taskpool.ets

    • Task.addDependency(第 ~1238 行):验证任务类型,通过 hasTaskDFS 检查循环依赖,并在 dependentTasks 集合中建立边
    • Task.removeDependency(第 ~1284 行):验证状态、移除边,并调整 taskDependenciesCount
    • Task.hasTaskDFS(第 ~1371 行):遍历 dependentTasks 进行 DFS 循环检测
    • InternalTask.waitForDependencies(第 ~718 行):在 taskDependenciesCount == 0 前通过 taskMutex/condVar 阻塞
    • InternalTask.notifyDependencies(第 ~731 行):递减每个依赖任务的计数、发出 condvar 信号,并激活已就绪任务
    • tryActivatePendingDependencyTask(第 ~3014 行):将依赖均已满足的任务重新加入队列
    • moveToPendingDependencyQueue(第 ~2993 行):在任务出队但仍被阻塞时,将任务和优先级存入待处理映射
  • 动态参考:commonlibrary/ets_utils/js_concurrent_module/taskpool/task.cpp 和 task_manager.cpp

    • Task::AddDependency、Task::RemoveDependency、Task::CheckAddDependency
    • TaskManager::StoreTaskDependency、TaskManager::CheckCircularDependency、TaskManager::NotifyDependencyTaskInfo、TaskManager::RemoveTaskDependency、TaskManager::ClearDependentTask
  • 测试参考:runtime_core/static_core/plugins/ets/tests/ets-common-tests/taskpool/common_tasks.ets

    • dependentTasksTest、circularDependencyTest、dependencyExecutedTaskTest、dependencyCancelTaskTest、doubleCancelByDependencyTest

有关数据结构、错误码和完整生命周期的详细参考,请阅读此技能目录中的 references/dependency_reference.md。

分析工作流

请按顺序执行以下步骤。不要跳过或合并步骤。

步骤 1:对问题进行分类

阅读用户的描述或失败的测试用例,并将问题归入以下某一类别:

类别常见症状错误码
循环依赖addDependency 立即抛出异常10200026
缺少/不存在的依赖项removeDependency 抛出异常10200027、10200097
依赖任务始终无法唤醒已提交任务但始终不执行;无声挂起无(不会抛出错误)
取消传播失败取消前置任务不会取消依赖任务,反之亦然无
非法 API 调用顺序对错误类型的任务调用 addDependency/removeDependency10200070、10200071、10200073、10200074、10200079、10200113
依赖后重新执行重新执行已经带依赖运行过的任务10200072
超时与依赖冲突对带有依赖项的任务调用 execute(task, {timeout})自定义错误

如果用户提供了测试用例(.ets 文件),请先阅读,并提取:任务创建顺序、addDependency 调用、execute 调用,以及预期行为与实际行为。

步骤 2:根据用户代码构建依赖图

从用户代码或测试用例中,按顺序提取所有 Task 创建、addDependency、removeDependency 和 execute 调用。构建有向图:

  • 节点:每个 Task 实例(如果有,使用变量名 + taskId 标记)
  • 边:每个 addDependency 都会创建一条边 dependent → prerequisite(依赖任务会等待前置任务)
  • 边删除:每个 removeDependency 都会移除对应的边
  • 执行状态:根据 execute/cancel 调用,将节点标记为 unexecuted、submitted、running、finished、canceled

以文本形式表示图,例如:

task1 ──depends on──> task2 ──depends on──> task3
task4 ──depends on──> task2

如果通过 removeDependency 修改了图,请展示每个重要状态变化时的图。

步骤 3:检测循环和非法边

在图上执行以下检查:

3a:循环检测

从每个节点开始使用 DFS。如果某条路径返回起始节点,则存在循环。报告确切的循环路径:

Cycle detected: task1 → task2 → task3 → task1
Introduced by: task3.addDependency(task1) at line X

静态实现使用 hasTaskDFS 遍历 dependentTasks(反向遍历:从前置任务指向其依赖任务)。确保循环检测遵循此遍历方向。

3b:非法任务类型的边

根据以下规则检查每个节点(这些是 addDependency 和 removeDependency 中的确切检查条件):

任务类型能否调用 addDependency?能否作为依赖目标?能否调用 removeDependency?
未执行(TASK)是是是(如果 isDependent)
已执行(COMMON_TASK,执行后)否(10200070)否(10200079)否(10200071)
周期性任务否(10200113)否(10200113)否(10200113)
组任务否(10200073)否(10200074)不适用
SeqRunner 任务否(10200070)否(10200079)否(10200071)
AsyncRunner 任务否(10200070)否(10200079)否(10200071)
超时任务否否不适用

报告所有违规,并注明具体错误码。

3c:缺少依赖边

对于 removeDependency 错误:确认移除时该边确实存在于图中。如果用户代码调用 removeDependency(taskX),但 taskX 不是调用方的前置任务,则会产生错误 10200027。

步骤 4:追踪通知/激活路径

对于“依赖任务始终无法唤醒”的问题,这是最重要的一步。从前置任务完成时开始,追踪完整路径,直到依赖任务应当执行为止:

Prerequisite finishes
  → InternalTask.execute() finally block
    → notifyDependencies() called
      → Iterates over prerequisite.dependentTasks
        → For each dependent:
          - Locks dependent.taskMutex
          - Decrements dependent.taskDependenciesCount
          - Signals dependent.condVar
          - If taskDependenciesCount == 0:
            → tryActivatePendingDependencyTask(dependent)
              - Takes priority from pendingDependencyTasks map
              - Re-enqueues dependent into globalTaskQueue
              - Signals global condvar to wake a worker

检查每个步骤是否可能失败:

失败点症状根本原因模式
未调用 notifyDependencies依赖任务始终无法唤醒任务类型不是 COMMON_TASK(动态模式:TriggerTask 只会为普通任务调用 NotifyDependencyTaskInfo);或者任务已取消(isCancel=true 会跳过通知)
dependentTasks 集合为空未通知任何依赖任务边在执行前已被移除,或者在 execute 之后才调用 addDependency
taskDependenciesCount 未降至 0依赖任务被唤醒后又重新等待某个前置任务完成时没有递减该依赖任务的计数(竞态:通知运行后才递增计数)
pendingDependencyTasks 中没有该依赖任务的条目tryActivatePendingDependencyTask 返回 false任务从未移入待处理队列(例如,任务已出队,并在所有前置任务完成之前开始 waitForDependencies)
任务在 tryActivatePendingDependencyTask 中处于 CANCELED 状态Promise 因取消错误而被拒绝另一个线程/任务在通知与激活之间取消了该依赖任务
全局 condvar 未发出信号Worker 不会提取重新入队的任务入队与 Worker 休眠之间发生竞态

诊断“始终无法唤醒”的问题时,请在以下位置添加日志:

  1. notifyDependencies:记录每个依赖任务的 taskId 和新的 taskDependenciesCount
  2. tryActivatePendingDependencyTask:记录是否找到优先级、任务是否处于 CANCELED 状态,以及重新入队是否成功
  3. waitForDependencies:记录开始等待和结束等待时的情况(包括最终的 taskDependenciesCount)
  4. moveToPendingDependencyQueue:记录任务移入待处理队列的情况,以及跳过任务的情况(依赖关系已清除)
步骤 5:追踪取消传播

任务被取消时,其依赖任务也应当被取消。追踪这条路径:

cancel(task) called
  → task state -> CANCELED (CAS)
  → tryCancelDependentTasks(task)
    → Iterates over InternalTask.of(task).dependentTasks
      → For each dependent:
        - calls cancel(dependent) (silently catches errors)
  → cleanupCanceledPendingDependencyTask(task)
    → Takes priority from pendingDependencyTasks
    → If found: finishCanceledPendingDependencyTask (rejects promises)

检查以下情况:

  • 依赖任务未被取消:dependentTasks 集合为空,或者边已被移除
  • 依赖任务卡在 WAITING 状态:它们位于全局队列中,而 cancel 未将其移除(动态模式下应由 RemoveDependTaskByTaskId 处理,静态模式下应由 tryCancelDependentTasks 处理)
  • 重复取消竞态:同一依赖链中的两个任务被并发取消——检查 cancel() 是否在状态转换时正确使用 CAS
步骤 6:生成诊断报告

输出一份包含以下部分的结构化报告:

依赖图
Nodes: [task1 (unexecuted), task2 (finished), task3 (submitted, isDependent=true)]
Edges:
  task3 → task2  (task3 depends on task2)
  task1 → task3  (task1 depends on task3)
发现的问题

对于每个问题,请提供:

  • 问题类型:循环/非法边/缺失边/通知失败/取消失败
  • 错误代码(如适用)
  • 准确的源代码位置(文件:行)
  • 受影响的边或路径
  • 根本原因说明
建议的修复方案

对于每个问题,建议采用以下其中一种方案:

  • 调整 addDependency 调用顺序:在 execute 之前声明所有依赖关系
  • 移除问题边:在任务类型更改之前使用 removeDependency
  • 拆分任务:创建一个尚未执行的新任务,而不是重复使用已执行的任务
  • 替换任务类型:需要依赖关系时,不要使用 periodic/group/seqRunner/asyncRunner
  • 添加同步机制:如果问题是通知/激活竞态,请指出具体的竞态时间窗口及修复方案(不要建议使用 sleep/retry/timeout)
  • 修复通知路径:如果未调用 notifyDependencies,请指出原因并修复相关条件
重构代码示例

如果用户的代码存在结构性问题(例如,在 execute 之后添加依赖关系、创建自引用循环,或在依赖链中使用组任务),请提供一段修正后的代码示例,展示正确的操作顺序。

关键不变量检查清单

分析任何任务池依赖问题时,请确认以下不变量均成立。如果有任何一项被违反,它就是根本原因或促成因素:

  1. 边方向不变量:addDependency(taskA, taskB) 表示 taskA 等待 taskB。边存储在 taskB.dependentTasks 中(反向映射)。如果遍历 dependentTasks,遍历方向是从前置任务到依赖任务,而不是从依赖任务到前置任务。

  2. taskDependenciesCount 不变量:对于任意任务 T,T.taskDependenciesCount 必须等于指向 T 的前置任务边数。如果 taskDependenciesCount > 0,T 必须位于 pendingDependencyTasks 中(如果已从全局队列出队),或者位于全局队列中(如果刚提交)。如果 taskDependenciesCount == 0,T 不得位于 pendingDependencyTasks 中。

  3. 任务类型限制不变量:只有尚未执行的 TASK 类型和 FUNCTION_TASK 才能参与 addDependency。禁止周期任务、组任务、seqRunner、asyncRunner 以及已执行的(COMMON_TASK)类型参与依赖关系的任一端。

  4. 通知不变量:COMMON_TASK 完成时(未取消),必须调用 notifyDependencies(),且必须遍历 this.dependentTasks 中的所有条目。若有任何条目缺失,某个依赖任务将永远无法唤醒。

  5. 取消传播不变量:任务取消时,其 dependentTasks 中的所有任务也必须递归取消。若有任何依赖任务未取消,它将一直挂起(等待已取消的前置任务),或在缺少前置任务结果的情况下执行。

  6. 单实例不变量:满足依赖条件后,waitForDependencies() 会检查 isRunning。如果同一任务的另一个实例正在运行,当前实例会等待。notifyDependencies() 会设置 isRunning = false 并通知 condVar,以解除下一个实例的阻塞。

  7. 待处理队列一致性:任务会从全局队列移动到 pendingDependencyTasks(出队但受阻时),再由 tryActivatePendingDependencyTask 重新加入全局队列。任务不得同时位于全局队列和 pendingDependencyTasks 中。execute() 开始时调用 removePendingDependencyTask 可确保完成清理。

需要留意的反模式

用户代码中出现以下模式时,几乎总是存在错误。请立即指出:

  1. 执行后调用 addDependency:在 taskpool.execute(task) 之后调用 task.addDependency(other)——抛出 10200070
  2. 自依赖:task.addDependency(task)——抛出 10200026
  3. 依赖链中包含周期任务:periodicTask.addDependency(x) 或 x.addDependency(periodicTask)——抛出 10200113
  4. 依赖链中包含组任务:组任务不能添加或接收依赖——抛出 10200073/10200074
  5. 缺少前置任务通知:如果通过 seqRunner/asyncRunner/group 执行前置任务,它将永远不会调用 notifyDependencies,因此依赖任务会一直挂起
  6. 依赖完成后重新执行:任务已带依赖运行后,再调用 taskpool.execute(task)——抛出 10200072
  7. 对错误的任务调用 removeDependency:移除不存在的依赖——抛出 10200027
  8. 取消时未传播:取消前置任务,却期望依赖任务仍能运行——依赖任务也会被取消

错误代码速查

代码常量含义常见修复方法
10200026CIRCULAR_DEPENDENCY依赖图中存在循环移除导致循环闭合的边
10200027DEPENDENCY_NOT_EXIST对非边调用 removeDependency仅移除实际添加过的边
10200070EXECUTED_TASK_ADD_DEPENDENCY对已执行的任务或 runner 任务调用 addDependency在执行前声明依赖
10200071EXECUTED_TASK_REMOVE_DEPENDENCY对已执行的任务调用 removeDependency执行后不要修改依赖
10200073GROUP_TASK_ADD_DEPENDENCY组任务调用 addDependency将组任务从依赖链中移除
10200074GROUP_TASK_RELIED_ON组任务作为依赖目标使用常规任务,而非组任务
10200079SEQ/ASYNC_RUNNER_TASK_RELIED_ONSeqRunner/AsyncRunner 作为依赖目标改用常规任务
10200097NO_DEPENDENCY对没有依赖项的任务调用 removeDependency确保先调用 addDependency
10200101NO_PARAMS不带参数调用 addDependency/removeDependency提供任务参数
10200113PERIODIC_TASK_ADD_DEPENDENCY依赖项中包含周期性任务将周期性任务从依赖链中移除

评论

支持 Markdown