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
·
Your Podcast. Everywhere. Effortlessly.
Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
→
LINE Digital Frontier - TECH
September 21, 2025
Technology
2.8k
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
92
Kotlin言語仕様書へ招待 〜コード「なぜ」を読み解く〜
ldf_tech
0
1.3k
累計5000万DLサービスの裏側 – LINEマンガのKotlinで挑む大規模 Server-side ETLの最適化
ldf_tech
2
510
Android端末で実現するオンデバイスLLM 2025
ldf_tech
0
81
How LINE MANGA Uses ClickHouse for Real-Time AnalysisSolving Data Integration Challenges with ClickHouse
ldf_tech
0
470
会社紹介資料
ldf_tech
1
6.1k
SwiftSyntaxでUIKitとSwiftUIの使用率を完璧に計測できちゃう件について
ldf_tech
0
420
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
OpenTelemetry eBPF Instrumentationの舞台裏 / Behind the Scenes of OpenTelemetry eBPF Instrumentation
ymotongpoo
4
1.1k
アプリログインとWeb認証基盤をつなぐ ASWebAuthenticationSession 作法
shimastripe
1
210
2026/09/10 Spring Bootから Jakarta EE/MicroProfileへの移行
megascus
0
340
Snowflakeのコスト最適化を支えるアーキテクチャ設計
ktatsuya
1
1.5k
2026_devsumi_ozono.pdf
o3
3
410
アリアドネの糸と、20年ごとの建て替え ── 長尾真『電子図書館』を、伊勢で読み直す / Rereading Makoto Nagao’s "Electronic Library" in Ise
ykiyota
0
160
10分で知る最近のOmarchy
komagata
0
260
全員がプロダクトへ向き合う組織を持続成長させるために——組織づくりのフライホイールと4象限 / The Flywheel Model and Four Quadrants for Organizational Design
hiro_torii
4
980
Where Is JetBrains AI Heading- — Central CLI, Air Alpha, and the Agentic Development Stack
x5gtrn
PRO
0
160
Microsoft 365 Copilot chat -tekoälypalvelun tietosuojaongelmat
hponka
0
730
ペアプロの価値はコードを書くことだけじゃない
codmoninc
PRO
0
200
AIエージェントの自己改善をどう設計するか / How to Design Self-Improvement for AI Agents
22mi
3
2k
Featured
See All Featured
How GitHub (no longer) Works
holman
316
150k
JAMstack: Web Apps at Ludicrous Speed - All Things Open 2022
reverentgeek
1
590
Designing Powerful Visuals for Engaging Learning
tmiket
1
540
SEO for Brand Visibility & Recognition
aleyda
0
4.7k
Exploring anti-patterns in Rails
aemeredith
3
500
Creating an realtime collaboration tool: Agile Flush - .NET Oxford
marcduiker
35
2.6k
From Legacy to Launchpad: Building Startup-Ready Communities
dugsong
0
330
SERP Conf. Vienna - Web Accessibility: Optimizing for Inclusivity and SEO
sarafernandez
2
1.6k
Reality Check: Gamification 10 Years Later
codingconduct
0
2.3k
BBQ
matthewcrist
89
10k
Making Projects Easy
brettharned
120
6.7k
Information Architects: The Missing Link in Design Systems
soysaucechin
1
1.1k
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.