This commit is contained in:
2025-11-12 16:49:19 +08:00
parent fea22aecab
commit 2f4205ad1a
7 changed files with 144 additions and 18 deletions

View File

@@ -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;

View File

@@ -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