Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
5000万ダウンロードを超える漫画サービスを支えるログ基盤の設計開発の全て
Search
Sponsored
·
Ship Features Fearlessly
Turn features on and off without deploys. Used by thousands of Ruby developers.
→
LINE Digital Frontier - TECH
September 21, 2025
Technology
2.9k
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
5000万ダウンロードを超える漫画サービスを支えるログ基盤の設計開発の全て
「iOSDC Japan 2025」の登壇資料です。
https://iosdc.jp/2025/
LINE Digital Frontier - TECH
September 21, 2025
More Decks by LINE Digital Frontier - TECH
See All by LINE Digital Frontier - TECH
是時候了!將RxSwift 程式碼全部改寫成 Swift Concurrency吧!/slides_iplayground_2026_20min_static
ldf_tech
0
150
Kotlin言語仕様書へ招待 〜コード「なぜ」を読み解く〜
ldf_tech
0
1.4k
累計5000万DLサービスの裏側 – LINEマンガのKotlinで挑む大規模 Server-side ETLの最適化
ldf_tech
2
570
Android端末で実現するオンデバイスLLM 2025
ldf_tech
0
84
How LINE MANGA Uses ClickHouse for Real-Time AnalysisSolving Data Integration Challenges with ClickHouse
ldf_tech
0
490
会社紹介資料
ldf_tech
1
6.2k
SwiftSyntaxでUIKitとSwiftUIの使用率を完璧に計測できちゃう件について
ldf_tech
0
430
Kotlin 2.0が与えるAndroid開発の進化
ldf_tech
0
340
Road to Kotlin 〜10年続くPerl運用からの脱却〜
ldf_tech
0
140
Other Decks in Technology
See All in Technology
自主式軟體工廠
philipz
0
120
私の推しは「聞いてから進む」AIです -AI-DLCに一人でアプリを作らせた話
yama3133
1
160
覗いてみよう 関数型ビジュアル言語×2Dグラフィックスの世界
yohyamasaki
0
170
Claude Designがめちゃくちゃ便利なので使ってほしい
diggymo
0
240
BedrockとLambdaで作る リアルタイム進行型推理ゲーム
kawametho
0
160
Codex概要
ymiya55
0
240
Amazon Bedrock Agents ClassicからAmazon Bedrock AgentCoreへ移行した際、ガードレール設定が2箇所に割れた話
matsunobu
0
170
OpenClawでAzure DevOpsのWiki更新を自動化する - クラウドAIだけでは届かない場所へ
yutakaosada
0
140
プラットフォームを「作る」、 チームに「入り込む」
sansantech
PRO
0
540
LLM機能を自作して分かるSnowflake Cortex AIの強み
nayuts
0
210
雪かき部 #7 もう怖くない!SELECT文!
foursue
0
270
AI臭い文章とは何なのか
nasuvitz
38
77k
Featured
See All Featured
Balancing Empowerment & Direction
lara
6
1.3k
WENDY [Excerpt]
tessaabrams
14
40k
BBQ
matthewcrist
89
10k
Thoughts on Productivity
jonyablonski
76
5.4k
Abbi's Birthday
coloredviolet
4
10k
How to Think Like a Performance Engineer
csswizardry
28
2.9k
For a Future-Friendly Web
brad_frost
183
10k
Winning Ecommerce Organic Search in an AI Era - #searchnstuff2025
aleyda
2
2.2k
Heart Work Chapter 1 - Part 1
lfama
PRO
10
36k
SEOcharity - Dark patterns in SEO and UX: How to avoid them and build a more ethical web
sarafernandez
0
290
Unsuck your backbone
ammeep
672
58k
Sharpening the Axe: The Primacy of Toolmaking
bcantrill
46
3k
Transcript
5000 支 iOSDC Japan 2 02 5 202 5 .
0 9 . 21 10 : 3 0 Track C @dsxsxsxs © LINE Digital Frontier Corporation
自己 人 日 LINE Digital Frontier VTuber 🏍💨 @dsxsxsxs
日 行 Swift 6 . 1 . 0 行 Firebase
Remote Config
Source Repository: https://github.com/dsxsxsxs/Tracker
行
None
None
SDK
None
None
行 面 Screen Impression Tap etc … UX ⾒ 方
行 行 行 行
力
Tracker Concurrent 非 Queueing, Priority: Utility gzip, deflate Exponential Backoff
[String: Sendable] Swift Package swift-tools-version: 6 . 1 . 0
Swift 6 Sendable ⾒ actor Sendable ⾒ ⾒ @unchecked Sendable
@unchecked Sendable @MainActor
Digest Loop 一 while dataStore.haveData() { let dataToSend = dataStore.getData(count:
chunkCount) do { try await networkClient.send(data: dataToSend) dataStore.deleteData(ids: dataToSend.map(\.id)) } catch { stopDigesting() break } }
None
Entity(Domain Object) TrackingData Use Case(Interactor, Controller) TrackingDispatcher Data Layer TrackingSQLiteDataStore,
TrackingNetworkClient Public Interface Tracker
Clean
Clean
Clean
public struct TrackingData: Sendable { let id: Int let data:
Data } Entity - TrackingData
Use Case
public protocol TrackingDispatcherProtocol: Sendable { func sendLogs(name: String, payloads: [[String:
Sendable]]) } public protocol TrackingDataStoreProtocol: Sendable { func save(data: [Data]) throws -> [TrackingData] func getData(count: Int) throws -> [TrackingData] func deleteData(ids: [Int]) throws } public protocol TrackingNetworkClientProtocol: Sendable { func send(data: [TrackingData]) async throws } Use Case All Sendable!!!!!
Data Layer TrackingSQLiteDataStore TrackingNetworkClient Public Interface Tracker
DataStore
DataStore SQLite (String ) private let tableName = "Tracking" private
let primaryKey = "id" private let dataKey = "rawData" CREATE TABLE IF NOT EXISTS \(tableName) ( \(primaryKey) INTEGER PRIMARY KEY AUTOINCREMENT, \(dataKey) BLOB );
DataStore - SQLite iOS
DataStore - Sendable import SQLite3 public final class TrackingSQLiteDataStore: TrackingDataStoreProtocol
{ private var db: OpaquePointer? public func save(data: [Data]) throws -> [TrackingData] { public func selectLogs(limit: Int) throws -> [TrackingData] { public func deleteLogs(ids: [Int]) throws { } Stored property 'db' of 'Sendable'-conforming class 'TrackingSQLiteDataStore' has non-sendable type 'OpaquePointer?' OpaquePointerSendableͰͳ͍ OSAllcatedUnfairLockͰϥοϓͯ͠μϝ SE 03 31 - Remove Sendable conformance from unsafe pointer types
DataStore final class DatabasePointer: @unchecked Sendable { private let lock
= NSRecursiveLock() private var _pointer: OpaquePointer? var pointer: OpaquePointer? { get { defer { lock.unlock() } lock.lock() return _pointer } set { defer { lock.unlock() } lock.lock() _pointer = newValue } } } Workaround 😅
DataStore public final class TrackingSQLiteDataStore: TrackingDataStoreProtocol { private let databasePointer
= DatabasePointer() private var db: OpaquePointer? { get { databasePointer.pointer } set { databasePointer.pointer = newValue } } public func save(data: [Data]) throws -> [TrackingData] {} public func getData(count: Int) throws -> [TrackingData] {} public func deleteData(ids: [Int]) throws {} }
DataStore - Test private let sut = TrackingSQLiteDataStore() @Suite(.serialized) final
class TrackingDataStoreTests { func save10Logs() throws { // sutʹLogΛ10݅อଘ.... } func delete10Logs() throws { // sutʹLogΛ10݅আ.... } } Swift Testing Concurrent
NetworkClient
NetworkClient Networking 入 5KB body protocol TrackingNetworkClientNetworking: Sendable { func
execute(request: URLRequest) async throws -> (Data, URLResponse) } private let minimumDeflateSize: Int32 = 5120
NetworkClient public final class TrackingNetworkClient: TrackingNetworkClientProtocol { let networking: TrackingNetworkClientNetworking
let suspend: @Sendable (Int) async throws -> Void let maxRetryCount: Int public func send(data: [TrackingData]) async throws {} } Networking retry 入 5KB body
NetworkClient - Send - Compression public func send(data: [TrackingData]) async
throws { var request = URLRequest(url: urlComponents.url!) // URLRequestΛઃఆ.... let rawData: Data = try data.asData() if rawData.count > minimumDeflateSize, let deflated = try? rawData.deflated() { request.httpBody = deflated request.setValue("deflate", forHTTPHeaderField: "Content-Encoding") request.setValue("\(deflated.count)", forHTTPHeaderField: "Content-Length") } else { request.httpBody = rawData request.setValue("\(rawData.count)", forHTTPHeaderField: "Content-Length") }
NetworkClient - Send - Exponential Backoff var retryAttempts = 0
repeat { do { if retryAttempts > 0 { let delay = pow(2.0, Double(retryAttempts)) try await suspend(Int(delay)) } try await networking.execute(request: request) break } catch { self.holdError(error) retryAttempts += 1 } } while retryAttempts <= maxRetryCount try throwErrorIfNeeded() }
NetworkClient - Compression import zlib extension Data { private static
let chunk = 1 << 14 // 16384bytes func deflated() throws -> Data { var stream = z_stream() var status: Int32 // লུ var data = Data(capacity: Self.chunk) // লུ data.count = Int(stream.total_out) return data } } zlib 用 Gzip, deflate Compression Framework
final class TrackingNetworkClientTests: @unchecked Sendable { var suspendSeconds: [Int] =
[] private var networking: ImmediateNetworking! private var sut: TrackingNetworkClient! init() { self.networking = ImmediateNetworking(statusCode: 200) sut = TrackingNetworkClient( maxRetryCount: 3, suspend: { self.suspendSeconds.append($0) }, networking: networking ) } NetworkClient - Test
NetworkClient - Test @Test func failHTTP400ThenRetry3Times() async throws { //
লུ await #expect { try await self.sut.send(data: oneData) } throws: { error in let nsError = error as NSError return nsError.code == 400 } #expect(networking.receivedRequests.count == 4) #expect(suspendSeconds == [2, 4, 8]) }
Dispatcher
Dispatcher actor TrackingDispatcher: TrackingDispatcherProtocol { private let dataStore: TrackingDataStoreProtocol private
let networkClient: TrackingNetworkClientProtocol private let errorHandler: TrackingErrorHandler private(set) var digestingTask: Task<Void, Never>? var isDigesting: Bool { digestingTask != nil } }
Dispatcher actor TrackingDispatcher: TrackingDispatcherProtocol { private let dataStore: TrackingDataStoreProtocol private
let networkClient: TrackingNetworkClientProtocol private let errorHandler: TrackingErrorHandler private let executor: LogSerialExecutor private(set) var digestingTask: Task<Void, Never>? let unownedExecutor: UnownedSerialExecutor var isDigesting: Bool { digestingTask != nil } public init(dataStore: TrackingDataStoreProtocol, networkClient: TrackingNetworkClientProtocol, errorHandler: @escaping TrackingErrorHandler) { self.dataStore = dataStore self.networkClient = networkClient self.errorHandler = errorHandler executor = LogSerialExecutor() self.unownedExecutor = executor.asUnownedSerialExecutor() }
Dispatcher - Executer private final class LogSerialExecutor: SerialExecutor { private
let serialQueue = DispatchQueue(label: "Executor", qos: .utility) nonisolated func enqueue(_ job: UnownedJob) { serialQueue.async { job.runSynchronously(on: self.asUnownedSerialExecutor()) } } func asUnownedSerialExecutor() -> UnownedSerialExecutor { UnownedSerialExecutor(ordinary: self) } Actor ⾒ 止
await Task.yield() Dispatcher - Digest Loop func startDigesting() { if
self.isDigesting { return } digestingTask = Task(priority: .utility) { [weak self] in guard let self else { return } var shouldContinue = true while shouldContinue { shouldContinue = await self.digestDataStore() sleep k } await self.stopDigesting() } }
Dispatcher private func digestDataStore() async -> Bool { guard isDigesting
else { return false } if Task.isCancelled { return false } do { let dataToSend = try dataStore.getData(count: 50) if dataToSend.isEmpty || Task.isCancelled { return false } try await networkClient.send(data: dataToSend) let ids = dataToSend.map { $0.id } try dataStore.deleteData(ids: ids) return true } catch { errorHandler(error) return false } } re-entry actor
Dispatcher func saveAndStartDigesting(data: [Data]) { do { _ = try
dataStore.save(data: data) startDigesting() } catch { errorHandler(error) } } private func stopDigesting() { digestingTask?.cancel() digestingTask = nil } Digest Loop Digest Loop Clean up
Dispatcher nonisolated func sendLogs(name: String, payloads: [[String: Sendable]]) { if
payloads.isEmpty { return } guard let encoded = try? Self.makeLogs(name: name, payloads: payloads) else { return } Task.detached(priority: .utility) { await self.saveAndStartDigesting(data: encoded) } } protocol ⾒ Loop
Dispatcher - Test final class TrackingDispatcherTests: @unchecked Sendable { private
let dataStore = MockDataStore() private let networkClient = MockNetworkClient() private var sut: TrackingDispatcher! init() throws { sut = .init( dataStore: dataStore, networkClient: networkClient, errorHandler: { _ in } ) }
Dispatcher - Test @Test func digestLoopStopWhenEmpty() async throws { let
data70 = Array(repeating: Data(), count: 70) _ = try dataStore.save(data: data70) dataStore.operations = [] await sut.saveAndStartDigesting(data: [Data()]) await sut.digestingTask?.value #expect(dataStore.operations == [ .insert, .select, .delete, .select, .delete, .select ]) #expect(networkClient.sentDataList.count == 2) #expect(networkClient.sentDataList.flatMap { $0 }.count == 71) } 70 入 1 入 Loop expect: 71 2
Dispatcher - Test func testConcurrentDigestCalls() async throws { // sutʹLogΛ10݅อଘ͓ͯ͘͠....
await withTaskGroup(of: Void.self) { group in group.addTask { await self.sut.saveAndStartDigesting(data: [Data()]) await self.sut.digestingTask?.value } group.addTask { await self.sut.startDigesting() } group.addTask { await self.sut.startDigesting() } } #expect(networkClient.sentDataList.flatMap { $0 }.count == 71) } expect: 71
Tracker
Tracker public final class Tracker: Sendable { let dispatcher: TrackingDispatcherProtocol
let dataStore: TrackingDataStoreProtocol, let networkClient: TrackingNetworkClientProtocol public func sendLogs(name: String, payloads: [[String: Sendable]]) { dispatcher.sendLogs(name: name, payloads: payloads) } }
Tracker 方 let tracker = Tracker( dataStore: try TrackingSQLiteDataStore(), networkClient:
try TrackingNetworkClient( maxRetryCount: 3, suspend: { try await Task.sleep(for: .seconds($0)) }, networking: TrackingNetworkClient.DefaultNetworking() ) ) tracker.sendLogs(name: "some_log_name", payloads: [ ["item_id": "12345", "event": "app_start"], ["item_id": "67890", "event": "app_end"] ]) Task.sleep URLSession
Test Coverage
None
行
Dual Send Adapter protocol OldLogger { func logEvents(name: String, parameters:
[[String: Any]]) } struct DualSendTracker: Sendable { let oldLogger: OldLogger let tracker: Tracker func sendLogs(name: String, parameters: [[String: Sendable]]) { oldLogger.logEvents(name: name, parameters: parameters) tracker.sendLogs(name: name, payloads: parameters) } }
😭
Call ⾒ 大 入
func sendLogs(name: String, parameters: [[String: Sendable]]) { oldLogger.logEvents(name: name, parameters:
parameters) tracker.sendLogs(name: name, payloads: editedParameters) } 大 入 var editedParameters: [[String: Sendable]] = parameters for (index, parameter) in editedParameters.enumerated() { if let someValue = parameter["some_key"] { editedParameters[index]["some_key"] = nil editedParameters[index]["some_new_key"] = someValue } if let someValue = parameter["some_key"] { editedParameters[index]["some_key"] = nil editedParameters[index]["some_new_key"] = someValue } if let someValue = parameter["some_key"] { editedParameters[index]["some_key"] = nil editedParameters[index]["some_new_key"] = someValue } if let someValue = parameter["some_key"] { editedParameters[index]["some_key"] = nil editedParameters[index]["some_new_key"] = someValue } if let someValue = parameter["some_key"] { editedParameters[index]["some_key"] = nil editedParameters[index]["some_new_key"] = someValue } if let someValue = parameter["some_key"] { editedParameters[index]["some_key"] = nil editedParameters[index]["some_new_key"] = someValue } if let someValue = parameter["some_key"] { editedParameters[index]["some_key"] = nil editedParameters[index]["some_new_key"] = someValue } if let someValue = parameter["some_key"] { editedParameters[index]["some_key"] = nil editedParameters[index]["some_new_key"] = someValue } if let someValue = parameter["some_key"] { editedParameters[index]["some_key"] = nil // ͦͷଞॾʑϚοϐϯά }
Call Call 生 Call 大
- 見
struct OldTapEventLog { let name: String let parameter: [String: Sendable]
func send() { oldLogger.logEvents( name: name, parameters: [parameter] ) } } 立 面 extension Tracker { struct TapEvent { let name: String let payload: [String: Sendable] func send() { tracker.sendLog( name: name, payload: payload ) } } }
struct OldTapEventLog { let name: String let parameter: [String: Sendable]
func send() { oldLogger.logEvents( name: name, parameters: [parameter] ) } func sendToNewTracker() -> Self { let tapEvent = Tracker.TapEvent(name: name, payload: parameter) tapEvent.send() return self } }
Call let tapEvent = OldTapEventLog( name: "home.like_button.tap", parameter: [ "item_id":
"12345" ]) tapEvent.sendToNewTracker().send()
Call let logName = "home.like_button.tap" let itemID = “12345" let
oldTapEvent = OldTapEventLog( name: logName, parameter: [ "item_id": itemID ] ) oldTapEvent.send() let tapEvent = Tracker.TapEvent( name: logName, payload: [ "new_item_id": itemID ] ) tapEvent.send() "item_id" "new_item_id"
大 人生 見 payload
None
行 見 小 止血 Hot Fix 小
行
行 方 二 止
Firebase Remote Config shouldSendLogToOldLogger: true default: false shouldSendLogToNewTracker: false default:
true 行 Config struct RemoteConfig { let shouldSendLogToOldLogger: Bool let shouldSendLogToNewTracker: Bool }
2024 10 月 true 100% v 2 4 . 1
1 . 1 0 false
2025 1 月 false 100% v 2 5 . 0
4 . 1 0 5 ⾒ 5 % 5 % 月 25 % 0 ~ 5 % 月 50 % 50 % 月 75 % 75 % GW 10 0 % 10 0 %
Hot Fix ++ 心 ++ Clean up
None
🏁
1 Digest Loop Clean SOLID Swift 6 自
2 行 見
3 Firebase Remote Config true false false true Config Free
⾒
CPU 用 用 ⾒ Disk InMemory Data Store Reachability
References Source Repository: https://github.com/dsxsxsxs/Tracker zlib doc: https://zlib.net/manual.html Compression Framework: https://developer.apple.com/documentation/
Accelerate/compressing-and-decompressing-data-with-buffer-compression
https://connpass.com/event/ 3 6 98 2 6 / 10 月 17
日 ( 金 ) 19:00 @ 木 11F
LINE 支
End Of doc.