在这里插入图片描述

在 API 耗时、CPU 使用率、磁盘 I/O 等性能指标分析中,我们经常会遇到这样的问题:

已经拿到一串按从小到大排序的样本值,如何快速定位「第一次超过阈值」的位置?
阈值附近有哪些样本点值得重点关注?

本案例基于 Kotlin Multiplatform(KMP)与 OpenHarmony,实现了一个有序阈值定位与二分查找分析器

  • 输入一条阈值 + 一串有序或无序数值序列
  • 自动排序样本,并用二分查找在 O(log n) 时间内定位阈值命中边界
  • 给出最后一个小于阈值、首个大于等于阈值的位置
  • 截取阈值附近的一段窗口,帮助你做 P95 / P99 等工程化分析
  • 通过 ArkTS 单页面板可视化展示分析结果。

一、问题背景与典型场景

在实际运维与性能分析中,常见需求包括:

  1. 接口耗时 P95 / P99 定位
    已经收集到某接口一批耗时样本,希望快速知道「第一个大于等于 200ms 的请求大概排在第几个」,从而估算 P95 / P99。

  2. 资源利用率告警阈值检查
    CPU 使用率长期采样后,希望判断「何时开始连续超过 80%」,以及阈值附近的抖动情况。

  3. 批量任务延迟分析
    批处理任务的执行时长按升序排序,想知道「多少任务在 SLO 阈值内完成」,以及「离阈值最近的几条任务」。

  4. 指标调参与可视化
    对监控系统的告警阈值进行调整时,需要看到阈值附近的真实样本分布,避免“刚好压在一大堆样本中间”导致误报。

这类问题可以统一抽象为:

给定有序数值序列 \( a_0 \le a_1 \le \dots \le a_{n-1} \) 和一个阈值 \( T \),
找到最后一个 \( a_i < T \) 与第一个 \( a_j \ge T \),并构造一个覆盖阈值附近若干点的窗口。


二、Kotlin 阈值二分查找分析引擎

1. 输入格式设计

为与现有案例保持一致,本案例继续采用简洁的「键值对 + 文本行」输入形式:

threshold=200
series=80,95,120,150,180,205,230,260
  • threshold:必填,表示阈值(支持整数或小数);
  • series:一串以逗号 / 空格 / 分号分隔的数值,可以是无序的,算法内部会自动排序。

也支持将 thresholdseries 写在一行或多行,只要保持 key=value 形式即可。


2. Kotlin 分析主入口

App.kt 中,我们定义了对外暴露的分析函数,并通过 @JsExport 让 OpenHarmony 端可以直接调用:

@JsExport
fun thresholdBinarySearchAnalyzer(inputData: String): String {
    val sanitized = inputData.trim()
    if (sanitized.isEmpty()) {
        return "❌ 输入为空,请按 threshold=200\\nseries=80,95,120,150,180,205,230 形式提供数据"
    }

    val lines = sanitized.lines()
        .map { it.trim() }
        .filter { it.isNotEmpty() }

    var threshold: Double? = null
    var series: List<Double> = emptyList()

    for (line in lines) {
        when {
            line.startsWith("threshold=", ignoreCase = true) -> {
                threshold = line.substringAfter("=").trim().toDoubleOrNull()
            }
            line.startsWith("series=", ignoreCase = true) -> {
                val rawNums = line.substringAfter("=").split(",", " ", ";")
                    .mapNotNull { it.trim().takeIf { s -> s.isNotEmpty() }?.toDoubleOrNull() }
                series = rawNums.sorted()
            }
        }
    }

    if (threshold == null) {
        return "❌ 未找到 threshold=... 配置,请提供告警阈值"
    }
    if (series.isEmpty()) {
        return "❌ 未解析到任何数值序列,请检查 series=1,2,3 的格式是否正确"
    }

    val t = threshold!!
    val firstGe = firstGreaterOrEqual(series, t)
    val lastLt = lastLessThan(series, t)

    val builder = StringBuilder()
    builder.appendLine("🎯 有序阈值定位与二分查找分析报告")
    builder.appendLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
    builder.appendLine("样本数量: ${series.size}")
    builder.appendLine("阈值: $t")
    builder.appendLine("最小值 / 最大值: ${series.first()} / ${series.last()}")
    builder.appendLine()

    if (lastLt == null && firstGe == null) {
        builder.appendLine("⚠️ 所有样本全部为 NaN 或无法比较,请检查输入")
        return builder.toString().trim()
    }

    when {
        lastLt == null -> {
            builder.appendLine("✅ 所有样本都大于等于阈值,阈值位于序列左侧之外")
            builder.appendLine("首个 ≥ 阈值的位置: 索引 0, 值=${series[0]}")
        }
        firstGe == null -> {
            builder.appendLine("✅ 所有样本都小于阈值,阈值位于序列右侧之外")
            builder.appendLine("最后一个 < 阈值的位置: 索引 ${series.lastIndex}, 值=${series.last()}")
        }
        else -> {
            builder.appendLine("📌 阈值在序列内部被命中或跨越")
            builder.appendLine("最后一个 < 阈值       : 索引 ${lastLt.index}, 值=${lastLt.value}")
            builder.appendLine("首个 ≥ 阈值 (含等于) : 索引 ${firstGe.index}, 值=${firstGe.value}")
            builder.appendLine("中间落差 Δ = ${(firstGe.value - lastLt.value).roundToInt()}")
        }
    }

    builder.appendLine()
    builder.appendLine("🔍 阈值附近窗口 (包含阈值邻近的 3~5 个点)")
    val window = buildThresholdWindow(series, firstGe?.index, lastLt?.index)
    window.forEachIndexed { i, v ->
        val globalIndex = window.startIndex + i
        val label = when {
            v == firstGe?.value -> "≥ 阈值边界"
            v == lastLt?.value -> "< 阈值边界"
            v > t -> "高于阈值"
            else -> "低于阈值"
        }
        builder.appendLine("  [全局索引 $globalIndex] 值=$v ($label)")
    }

    builder.appendLine()
    builder.appendLine("🧠 工程化解读")
    builder.appendLine("- 该分析可用于快速定位某条接口在 P95/P99 阈值处的大致位置;")
    builder.appendLine("- 二分查找保证在 O(log n) 时间内完成阈值命中点查找;")
    builder.appendLine("- 结合阈值附近窗口,可以进一步做细粒度的可视化与告警调参。")

    return builder.toString().trim()
}

这里有几个实现上的要点:

  • 输入解析对大小写不敏感(threshold / Threshold 均可);
  • series 内部自动 sorted(),避免调用方必须先排好序;
  • NaN 进行了简易的防御性处理,避免污染比较结果;
  • 返回值是多行字符串,适合 ArkTS Text + 等宽字体直接展示。

3. 二分查找实现细节

为了保持代码清晰,我们将边界查找拆成两个小函数:

private data class BoundaryHit(
    val index: Int,
    val value: Double
)

private fun firstGreaterOrEqual(series: List<Double>, threshold: Double): BoundaryHit? {
    var left = 0
    var right = series.lastIndex
    var resultIndex: Int? = null

    while (left <= right) {
        val mid = (left + right) ushr 1
        val value = series[mid]
        if (value.isNaN()) {
            // 跳过 NaN,向右收缩
            left = mid + 1
        } else if (value >= threshold) {
            resultIndex = mid
            right = mid - 1
        } else {
            left = mid + 1
        }
    }

    return resultIndex?.let { BoundaryHit(it, series[it]) }
}

private fun lastLessThan(series: List<Double>, threshold: Double): BoundaryHit? {
    var left = 0
    var right = series.lastIndex
    var resultIndex: Int? = null

    while (left <= right) {
        val mid = (left + right) ushr 1
        val value = series[mid]
        if (value.isNaN()) {
            // 跳过 NaN,向左收缩
            right = mid - 1
        } else if (value < threshold) {
            resultIndex = mid
            left = mid + 1
        } else {
            right = mid - 1
        }
    }

    return resultIndex?.let { BoundaryHit(it, series[it]) }
}

二分查找的核心思想是:

  • 每次取中点 mid,根据 series[mid] 与阈值 threshold 的大小关系,缩小左右边界;
  • 对于“首个 ≥ 阈值”,一旦命中就记录下标并继续向左收缩;
  • 对于“最后一个 < 阈值”,一旦命中就记录下标并继续向右收缩;
  • 最终记录下来的就是我们要找的边界点。

复杂度分析:

  • 单次查找时间复杂度:\( O(\log n) \);
  • 需要两次查找(一个 <,一个 ≥),总体仍然是 \( O(\log n) \);
  • 空间复杂度:除输入序列外,仅使用常数级局部变量,\( O(1) \)。

4. 阈值附近窗口构造

有了两个边界点之后,我们还希望得到一段「阈值附近的窗口」,方便在 ArkTS 侧做可视化高亮:

private data class ThresholdWindow(
    val values: List<Double>,
    val startIndex: Int
)

private fun buildThresholdWindow(
    series: List<Double>,
    firstGeIndex: Int?,
    lastLtIndex: Int?
): ThresholdWindow {
    if (series.isEmpty()) return ThresholdWindow(emptyList(), 0)

    // 选择一个中心点: 优先使用首个 ≥ 阈值 的索引,其次使用最后一个 < 阈值 的索引
    val center = firstGeIndex ?: lastLtIndex ?: 0
    val windowRadius = 2 // 左右各取 2 个点

    val start = max(0, center - windowRadius)
    val end = min(series.lastIndex, center + windowRadius)

    return ThresholdWindow(
        values = series.subList(start, end + 1),
        startIndex = start
    )
}

这里我们简单地在中心点左右各取 2 个样本点,拼成一个长度不超过 5 的窗口。
这个窗口可以直接在 ArkTS 界面中渲染成一列带标签的行,例如:

[全局索引 4] 值=180 (低于阈值)
[全局索引 5] 值=205 (≥ 阈值边界)
[全局索引 6] 值=230 (高于阈值)

三、OpenHarmony 侧调用与展示思路

在 OpenHarmony ArkTS 侧,可以像之前的案例一样直接从编译产物 hellokjs 中导出函数:

import { thresholdBinarySearchAnalyzer } from './hellokjs'

在页面中维护两个输入:

  • 阈值输入框(或滑块),用于设置 threshold
  • 多行文本输入,用于填写 series 样本数据。

点击「运行阈值分析」按钮时,拼出如下 payload:

const payload = `threshold=${this.threshold}\nseries=${this.seriesInput}`
this.result = thresholdBinarySearchAnalyzer(payload)

结果区域使用 Text + 等宽字体显示 Kotlin 返回的多行报告,并对「首个 ≥ 阈值」与「最后一个 < 阈值」等关键行进行高亮。


四、工程化实践建议

在真实工程中,可以基于本案例做进一步扩展:

  1. 支持百分位直接计算
    在已有二分查找的基础上,结合样本数量 n,直接计算 P90 / P95 / P99 对应的下标并高亮展示。

  2. 支持多阈值对比
    允许一次性输入多个阈值(例如 150 / 200 / 300),分别做边界查找并展示合并报告。

  3. 与时间序列案例联动
    将本案例集成到「时间序列趋势分段」「序列异常检测」等案例中,用于对异常区间进一步做阈值切分。

  4. 在终端设备侧离线运行
    利用 KMP + OpenHarmony 的跨端特性,把这套分析逻辑下沉到设备侧,在无网络时也能做本地阈值分析与调参验证。

通过这个小巧的二分查找案例,你可以把「算法教科书里的经典技巧」真正落地到 AIOps / 性能调优 / 监控告警等工程场景中,
既保持代码简单可读,又能在设备资源有限的环境中获得足够快的分析能力。

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐