Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
5000万ダウンロードを超える漫画サービスを支えるログ基盤の設計開発の全て
Search
LINE Digital Frontier - TECH
September 21, 2025
Technology
0
1.8k
5000万ダウンロードを超える漫画サービスを支えるログ基盤の設計開発の全て
「iOSDC Japan 2025」の登壇資料です。
https://iosdc.jp/2025/
LINE Digital Frontier - TECH
September 21, 2025
Tweet
Share
More Decks by LINE Digital Frontier - TECH
See All by LINE Digital Frontier - TECH
Kotlin言語仕様書へ招待 〜コード「なぜ」を読み解く〜
ldf_tech
0
360
累計5000万DLサービスの裏側 – LINEマンガのKotlinで挑む大規模 Server-side ETLの最適化
ldf_tech
1
270
Android端末で実現するオンデバイスLLM 2025
ldf_tech
0
41
How LINE MANGA Uses ClickHouse for Real-Time AnalysisSolving Data Integration Challenges with ClickHouse
ldf_tech
0
320
会社紹介資料
ldf_tech
1
3.9k
SwiftSyntaxでUIKitとSwiftUIの使用率を完璧に計測できちゃう件について
ldf_tech
0
340
Kotlin 2.0が与えるAndroid開発の進化
ldf_tech
0
280
Road to Kotlin 〜10年続くPerl運用からの脱却〜
ldf_tech
0
89
Kotlin Collection関数をマスター
ldf_tech
1
520
Other Decks in Technology
See All in Technology
巨大モノリスのリプレイス──機能整理とハイブリッドアーキテクチャで挑んだ再構築戦略
zozotech
PRO
0
240
Service Monitoring Platformについて
lycorptech_jp
PRO
0
330
アジャイル社内普及ご近所さんマップを作ろう / Let's create an agile neighborhood map
psj59129
1
140
現地速報!Microsoft Ignite 2025 M365 Copilotアップデートレポート
kasada
2
1.5k
TypeScript 6.0で非推奨化されるオプションたち
uhyo
13
4.3k
不確実性に備える ABEMA の信頼性設計とオブザーバビリティ基盤
nagapad
4
5.5k
AIと自動化がもたらす業務効率化の実例: 反社チェック等の調査・業務プロセス自動化
enpipi
0
760
社内外から"使ってもらえる"データ基盤を支えるアーキテクチャの秘訣/登壇資料(飯塚 大地・高橋 一貴)
hacobu
PRO
0
4.9k
Tomcatが起動しない!?SecureRandomと乱数デバイスの罠
fujikawa8
1
110
入社したばかりでもできる、 アクセシビリティ改善の第一歩
unachang113
2
340
組織の“見えない壁”を越えよ!エンタープライズシフトに必須な3つのPMの「在り方」変革 #pmconf2025
masakazu178
1
650
グローバルなコンパウンド戦略を支えるモジュラーモノリスとドメイン駆動設計
kawauso
3
6.8k
Featured
See All Featured
Code Review Best Practice
trishagee
72
19k
For a Future-Friendly Web
brad_frost
180
10k
Performance Is Good for Brains [We Love Speed 2024]
tammyeverts
12
1.3k
The Language of Interfaces
destraynor
162
25k
[RailsConf 2023 Opening Keynote] The Magic of Rails
eileencodes
31
9.8k
Measuring & Analyzing Core Web Vitals
bluesmoon
9
680
A designer walks into a library…
pauljervisheath
210
24k
VelocityConf: Rendering Performance Case Studies
addyosmani
333
24k
Bootstrapping a Software Product
garrettdimon
PRO
307
110k
Optimizing for Happiness
mojombo
379
70k
Building Flexible Design Systems
yeseniaperezcruz
329
39k
How To Stay Up To Date on Web Technology
chriscoyier
791
250k
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.