kmp openharmony 有序阈值定位与二分查找分析
有序阈值定位与二分查找分析器 本工具基于Kotlin Multiplatform实现,用于快速定位数值序列中首次超过阈值的样本点。主要功能包括: 自动排序:输入数值序列自动排序 二分查找:O(log n)时间复杂度定位阈值边界 阈值分析:找出最后一个小于阈值和首个大于等于阈值的样本点 窗口展示:截取阈值附近的样本区间 可视化支持:通过ArkTS单页面板展示分析结果 适用场景包括接口耗时P95/P9

在 API 耗时、CPU 使用率、磁盘 I/O 等性能指标分析中,我们经常会遇到这样的问题:
已经拿到一串按从小到大排序的样本值,如何快速定位「第一次超过阈值」的位置?
阈值附近有哪些样本点值得重点关注?
本案例基于 Kotlin Multiplatform(KMP)与 OpenHarmony,实现了一个有序阈值定位与二分查找分析器:
- 输入一条阈值 + 一串有序或无序数值序列;
- 自动排序样本,并用二分查找在 O(log n) 时间内定位阈值命中边界;
- 给出最后一个小于阈值、首个大于等于阈值的位置;
- 截取阈值附近的一段窗口,帮助你做 P95 / P99 等工程化分析;
- 通过 ArkTS 单页面板可视化展示分析结果。
一、问题背景与典型场景
在实际运维与性能分析中,常见需求包括:
-
接口耗时 P95 / P99 定位
已经收集到某接口一批耗时样本,希望快速知道「第一个大于等于 200ms 的请求大概排在第几个」,从而估算 P95 / P99。 -
资源利用率告警阈值检查
CPU 使用率长期采样后,希望判断「何时开始连续超过 80%」,以及阈值附近的抖动情况。 -
批量任务延迟分析
批处理任务的执行时长按升序排序,想知道「多少任务在 SLO 阈值内完成」,以及「离阈值最近的几条任务」。 -
指标调参与可视化
对监控系统的告警阈值进行调整时,需要看到阈值附近的真实样本分布,避免“刚好压在一大堆样本中间”导致误报。
这类问题可以统一抽象为:
给定有序数值序列 \( 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:一串以逗号 / 空格 / 分号分隔的数值,可以是无序的,算法内部会自动排序。
也支持将 threshold 与 series 写在一行或多行,只要保持 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 返回的多行报告,并对「首个 ≥ 阈值」与「最后一个 < 阈值」等关键行进行高亮。
四、工程化实践建议
在真实工程中,可以基于本案例做进一步扩展:
-
支持百分位直接计算
在已有二分查找的基础上,结合样本数量n,直接计算 P90 / P95 / P99 对应的下标并高亮展示。 -
支持多阈值对比
允许一次性输入多个阈值(例如 150 / 200 / 300),分别做边界查找并展示合并报告。 -
与时间序列案例联动
将本案例集成到「时间序列趋势分段」「序列异常检测」等案例中,用于对异常区间进一步做阈值切分。 -
在终端设备侧离线运行
利用 KMP + OpenHarmony 的跨端特性,把这套分析逻辑下沉到设备侧,在无网络时也能做本地阈值分析与调参验证。
通过这个小巧的二分查找案例,你可以把「算法教科书里的经典技巧」真正落地到 AIOps / 性能调优 / 监控告警等工程场景中,
既保持代码简单可读,又能在设备资源有限的环境中获得足够快的分析能力。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)