-

背景

近期有例上报框架导致的视频号卡顿问题,本篇文章记录针对该类反馈强化监控的方法。

反馈如上,即:在视频号单流,刚开始刷视频号没问题,越刷越卡。

检测出的fps有什么特征?

和常规卡顿不同,该bug导致的fps特征曲线是:刷了足够多的feed后,才开始出现卡顿,所以收集到的fps曲线走向是:

之前fps检测之后数据是按取平均的方式处理的,所以这种前大段正常,后面掉帧的情况,会被取平均掩盖低fps的情况。

当采集100条feed fps信息求均值后,后期数据比较差的fps就被平均了,导致整个fps数据波动看起来不明显。

卡顿指标的进一步优化

我们知道,卡顿从分类上大致可以分为三种类型:

  • 类型1:全局卡顿(从始至终都很卡)

  • 类型2:关键点卡顿(只有某个时间点、某个场景卡顿)

  • 类型3:越来越卡(使用时间越长越卡)

现有 fps报警 计算场景值主要覆盖了类型1,所以需要指标对类型2类型3也进行监测。

这里我们引入Google提出的Jank指标,这个指标已被多方验证有效,关于Jank指标的描述可以移步我之前写的文章:FPS与Jank卡顿指标

简单来说,可以这么一句话描述Jank指标:

1
如果连续N (N>=3?)帧帧间隔 > 40ms(对应25fps)

但只要使用过Jank卡顿指标的同学就会知道,Jank卡顿出现的情况非常频繁,有一些系统接口的回调也可能会导致出现Jank卡顿,以至于后果就变成Jank卡顿一直在上报,但开发人员并不关心。

为了避免这种情况,我们对Jank指标进行分级处理 :

  • 一级Jank告警:N = 9
  • 二级Jank告警:N = 6
  • 三级Jank告警:N = 3

逻辑实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
@interface WCFinderCommonFPSMonitor ()
@property (nonatomic, strong) CADisplayLink *link;
@property (nonatomic, assign) NSUInteger count;
@property (nonatomic, assign) NSTimeInterval lastTime;
@property (nonatomic, assign) NSTimeInterval lastFrameTime;

@property (nonatomic, assign) CGFloat duration;
@property (nonatomic, strong) NSMutableDictionary<NSString *, YYWeakProxy *> *tickObservers;

@end

@implementation WCFinderCommonFPSMonitor

- (instancetype)init {
if (self = [super init]) {
_tickObservers = [NSMutableDictionary dictionary];
_duration = 1;
}
return self;
}

- (void)dealloc {
[_link invalidate];
}

- (NSMutableArray<NSString *> *)fpsInfoArray {
if (!_fpsInfoArray) {
_fpsInfoArray = [NSMutableArray array];
}
return _fpsInfoArray;
}

- (NSMutableArray<NSString *> *)dropFrameInfoArray {
if (!_dropFrameInfoArray) {
_dropFrameInfoArray = [NSMutableArray array];
}
return _dropFrameInfoArray;
}

- (void)start {
[self stop];
_link = [CADisplayLink displayLinkWithTarget:[YYWeakProxy proxyWithTarget:self] selector:@selector(tick:)];
[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}

- (void)startWithDuration:(CGFloat)duration {
[self start];
self.duration = duration;
}

- (void)stop {
[_link invalidate];
_lastTime = 0;
_lastFrameTime = 0;
_count = 0;
}

- (void)addObserver:(id<WCFinderCommonFPSMonitorDelegate>)observer {
NSString *identifier = observer.observerIdentifier;
if (!identifier.length || [_tickObservers objectForKey:identifier] != nil) {
return;
}
[_tickObservers safeSetObject:[YYWeakProxy proxyWithTarget:observer] forKey:identifier];
}

- (void)tick:(CADisplayLink *)link {
if (_lastTime == 0) {
_lastTime = link.timestamp;
return;
}
_count++;
NSTimeInterval delta = link.timestamp - _lastTime;
NSTimeInterval frameInterval = link.timestamp - _lastFrameTime;

// 掉帧统计
NSInteger limitFrame = 25; // fps低于25计为掉帧
CGFloat limitDuration = 1.0 / limitFrame;
WCFinderDebug(@"finder fps frameInterval:%f", frameInterval);
if (frameInterval > limitDuration) {
NSMutableString *content = [NSMutableString string];
[content safeAppendString:[NSString stringWithFormat:@"%lu", (unsigned long)_count]];
[content safeAppendString:@";"];
[content safeAppendString:[NSString stringWithFormat:@"%u", (unsigned int)[CUtility genServerCurrentTime]]];
[self.dropFrameInfoArray safeAddObject:content];
}
_lastFrameTime = link.timestamp;

// fps计算
if (delta < self.duration) {
return;
}

_lastTime = link.timestamp;
float fps = _count / delta;
_count = 0;
self.curFps = fps;

NSMutableString *content = [NSMutableString string];
[content safeAppendString:[NSString stringWithFormat:@"%f", fps]];
[content safeAppendString:@";"];
[content safeAppendString:[NSString stringWithFormat:@"%u", (unsigned int)[CUtility genServerCurrentTime]]];
[self.fpsInfoArray safeAddObject:content];

[_tickObservers.allValues enumerateObjectsUsingBlock:^(YYWeakProxy *proxy, NSUInteger idx, BOOL *stop) {
[proxy.target onTick:fps];
}];
}