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
140
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
Android端末で実現するオンデバイスLLM 2025
ldf_tech
0
28
How LINE MANGA Uses ClickHouse for Real-Time AnalysisSolving Data Integration Challenges with ClickHouse
ldf_tech
0
290
会社紹介資料
ldf_tech
1
3.4k
SwiftSyntaxでUIKitとSwiftUIの使用率を完璧に計測できちゃう件について
ldf_tech
0
310
Kotlin 2.0が与えるAndroid開発の進化
ldf_tech
0
260
Road to Kotlin 〜10年続くPerl運用からの脱却〜
ldf_tech
0
70
Kotlin Collection関数をマスター
ldf_tech
1
480
Kotlin sealed classを用いた、 ユーザーターゲティングDSL(専用言語)と 実環境で秒間1,000万評価を行う処理系の事例紹介
ldf_tech
0
280
マンガアプリのメモリ改善と解析方法
ldf_tech
0
64
Other Decks in Technology
See All in Technology
Snowflake Intelligence × Document AIで“使いにくいデータ”を“使えるデータ”に
kevinrobot34
1
160
Oracle Base Database Service 技術詳細
oracle4engineer
PRO
10
75k
Terraformで構築する セルフサービス型データプラットフォーム / terraform-self-service-data-platform
pei0804
1
210
IoT x エッジAI - リアルタイ ムAI活用のPoCを今すぐ始め る方法 -
niizawat
0
160
dbt開発 with Claude Codeのためのガードレール設計
10xinc
3
1.5k
How AI agents are changing the way we should build APIs
fabpot
1
300
非エンジニアによるDevin開発のためにSREができること
shonansurvivors
0
110
Apache Icebergを体験しよう (2025.9.21)
simosako
3
370
DroidKaigi 2025 Androidエンジニアとしてのキャリア
mhidaka
2
410
Claude Code でアプリ開発をオートパイロットにするためのTips集 Zennの場合 / Claude Code Tips in Zenn
wadayusuke
8
4k
はじめてのOSS開発からみえたGo言語の強み
shibukazu
4
1.1k
slog.Handlerのよくある実装ミス
sakiengineer
4
560
Featured
See All Featured
The Straight Up "How To Draw Better" Workshop
denniskardys
236
140k
Imperfection Machines: The Place of Print at Facebook
scottboms
268
13k
Save Time (by Creating Custom Rails Generators)
garrettdimon
PRO
32
1.6k
Distributed Sagas: A Protocol for Coordinating Microservices
caitiem20
333
22k
Dealing with People You Can't Stand - Big Design 2015
cassininazir
367
27k
ピンチをチャンスに:未来をつくるプロダクトロードマップ #pmconf2020
aki_iinuma
127
53k
Large-scale JavaScript Application Architecture
addyosmani
513
110k
Done Done
chrislema
185
16k
Reflections from 52 weeks, 52 projects
jeffersonlam
352
21k
Raft: Consensus for Rubyists
vanstee
140
7.1k
Into the Great Unknown - MozCon
thekraken
40
2k
The Illustrated Children's Guide to Kubernetes
chrisshort
48
50k
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.