3
This commit is contained in:
@@ -38,6 +38,9 @@ typedef void (^KBStreamFetcherFinishHandler)(NSError *_Nullable error);
|
||||
/// 非 SSE 且一次性拿到大段文本时,是否按空格切词逐条回调(模拟“逐词流式”),默认 YES。
|
||||
@property (nonatomic, assign) BOOL splitLargeDeltasOnWhitespace;
|
||||
|
||||
/// 调试日志:默认 YES。输出起止时刻、首包耗时、各分片内容(截断)等关键信息。
|
||||
@property (nonatomic, assign) BOOL loggingEnabled;
|
||||
|
||||
// 回调(统一在主线程触发)
|
||||
@property (nonatomic, copy, nullable) KBStreamFetcherChunkHandler onChunk;
|
||||
@property (nonatomic, copy, nullable) KBStreamFetcherFinishHandler onFinish;
|
||||
|
||||
@@ -18,6 +18,12 @@
|
||||
@property (nonatomic, strong) NSMutableArray<NSString *> *pendingQueue; // 待回调的分片(节流输出)
|
||||
@property (nonatomic, strong) NSTimer *flushTimer; // 定时从队列取出一条回调
|
||||
@property (nonatomic, strong, nullable) NSError *finishError; // 结束时的错误(需要等队列清空再回调)
|
||||
|
||||
// Metrics
|
||||
@property (nonatomic, assign) CFAbsoluteTime tStart; // start() 被调用的时刻
|
||||
@property (nonatomic, assign) CFAbsoluteTime tFirstByte; // 第一次拿到可解码内容
|
||||
@property (nonatomic, assign) CFAbsoluteTime tFinish; // 完成/失败时刻
|
||||
@property (nonatomic, assign) NSInteger emittedChunkCount; // 已输出分片数量
|
||||
@end
|
||||
|
||||
// 计算数据中以 UTF-8 编码可完整解码的“前缀字节长度”,避免切断多字节字符
|
||||
@@ -60,6 +66,7 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
|
||||
_pendingQueue = [NSMutableArray array];
|
||||
_flushInterval = 0.1;
|
||||
_splitLargeDeltasOnWhitespace = YES;
|
||||
_loggingEnabled = YES;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -96,6 +103,18 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
|
||||
[self.flushTimer invalidate]; self.flushTimer = nil;
|
||||
self.finishError = nil;
|
||||
|
||||
self.tStart = CFAbsoluteTimeGetCurrent();
|
||||
self.tFirstByte = 0;
|
||||
self.tFinish = 0;
|
||||
self.emittedChunkCount = 0;
|
||||
if (self.loggingEnabled) {
|
||||
NSLog(@"[KBStream] start url=%@ acceptSSE=%@ disableCompression=%@ flush=%.0fms splitWords=%@",
|
||||
self.url.absoluteString,
|
||||
self.acceptEventStream?@"YES":@"NO",
|
||||
self.disableCompression?@"YES":@"NO",
|
||||
self.flushInterval*1000.0,
|
||||
self.splitLargeDeltasOnWhitespace?@"YES":@"NO");
|
||||
}
|
||||
self.task = [self.session dataTaskWithRequest:req];
|
||||
[self.task resume];
|
||||
}
|
||||
@@ -150,6 +169,15 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
|
||||
NSUInteger validLen = (self.textEncoding == NSUTF8StringEncoding)
|
||||
? kb_validUTF8PrefixLen(self.buffer)
|
||||
: self.buffer.length;
|
||||
if (validLen > 0 && self.tFirstByte == 0) {
|
||||
self.tFirstByte = CFAbsoluteTimeGetCurrent();
|
||||
if (self.loggingEnabled) {
|
||||
NSLog(@"[KBStream] first-bytes after %.0fms (encoding=%@, SSE=%@)",
|
||||
(self.tFirstByte - self.tStart)*1000.0,
|
||||
(self.textEncoding==NSUTF8StringEncoding?@"UTF-8":@"Other"),
|
||||
self.isSSE?@"YES":@"NO");
|
||||
}
|
||||
}
|
||||
if (validLen == 0) return; // 末尾可能卡着半个字符
|
||||
|
||||
if (self.isSSE) {
|
||||
@@ -181,12 +209,8 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
|
||||
NSString *v = [ln substringFromIndex:5];
|
||||
if (v.length > 0 && [v hasPrefix:@" "]) v = [v substringFromIndex:1];
|
||||
[payload appendString:v ?: @""];
|
||||
[payload appendString:@"\n"]; // 多 data 行合并
|
||||
}
|
||||
}
|
||||
if (payload.length > 0 && [payload hasSuffix:@"\n"]) {
|
||||
[payload deleteCharactersInRange:NSMakeRange(payload.length - 1, 1)];
|
||||
}
|
||||
if (payload.length > 0) { [self enqueueChunk:payload]; }
|
||||
}
|
||||
}
|
||||
@@ -228,17 +252,28 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
|
||||
NSString *v = [ln substringFromIndex:5];
|
||||
if (v.length > 0 && [v hasPrefix:@" "]) v = [v substringFromIndex:1];
|
||||
[payload appendString:v ?: @""];
|
||||
[payload appendString:@"\n"];
|
||||
}
|
||||
}
|
||||
if (payload.length > 0 && [payload hasSuffix:@"\n"]) {
|
||||
[payload deleteCharactersInRange:NSMakeRange(payload.length - 1, 1)];
|
||||
}
|
||||
if (payload.length > 0) {
|
||||
[self emitChunk:payload];
|
||||
NSString *delta = nil;
|
||||
if ((NSInteger)payload.length >= self.deliveredCharCount) {
|
||||
delta = [payload substringFromIndex:self.deliveredCharCount];
|
||||
} else {
|
||||
delta = payload;
|
||||
}
|
||||
self.deliveredCharCount = payload.length;
|
||||
if (delta.length > 0) { [self emitChunk:delta]; }
|
||||
}
|
||||
}
|
||||
|
||||
self.tFinish = CFAbsoluteTimeGetCurrent();
|
||||
if (self.loggingEnabled) {
|
||||
double t0 = (self.tFirstByte>0? (self.tFirstByte - self.tStart)*1000.0 : -1);
|
||||
double t1 = (self.tFirstByte>0? (self.tFinish - self.tFirstByte)*1000.0 : -1);
|
||||
double tt = (self.tFinish - self.tStart)*1000.0;
|
||||
NSLog(@"[KBStream] finish chunks=%ld firstByte=%.0fms after start, tail=%.0fms, total=%.0fms error=%@",
|
||||
(long)self.emittedChunkCount, t0, t1, tt, error);
|
||||
}
|
||||
// 若队列还有待输出内容,等队列清空再回调 finish
|
||||
if (self.pendingQueue.count > 0) {
|
||||
self.finishError = error;
|
||||
@@ -280,6 +315,11 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
|
||||
text = [text stringByReplacingOccurrencesOfString:@"\t " withString:@"\t"];
|
||||
}
|
||||
if (text.length == 0) { self.lastChunkEndedWithTab = NO; return; }
|
||||
self.emittedChunkCount += 1;
|
||||
if (self.loggingEnabled) {
|
||||
NSLog(@"[KBStream] chunk#%ld len=%lu text=\"%@\"",
|
||||
(long)self.emittedChunkCount, (unsigned long)text.length, KBPrintableSnippet(text, 160));
|
||||
}
|
||||
if (self.onChunk) dispatch_async(dispatch_get_main_queue(), ^{ self.onChunk(text); });
|
||||
self.hasEmitted = YES;
|
||||
// 记录末尾是否为分段分隔符 \t(用于跨分片处理)
|
||||
@@ -317,4 +357,15 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Logging helpers
|
||||
|
||||
static NSString *KBPrintableSnippet(NSString *s, NSUInteger maxLen) {
|
||||
if (!s) return @"";
|
||||
NSString *x = [s stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
|
||||
if (x.length > maxLen) {
|
||||
x = [[x substringToIndex:maxLen] stringByAppendingString:@"…"];
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
Reference in New Issue
Block a user