From ebe56401275348e3d1988ce072545e5f25983234 Mon Sep 17 00:00:00 2001 From: vakharwalad23 Date: Sun, 20 Sep 2026 12:57:00 +0530 Subject: [PATCH 1/9] perf(asr): bulk-fill MLMultiArray resets and copies MLArrayCache.returnArray reset the 240000-sample preprocessor input one NSNumber at a time, about 20 ms per transcription on the path that returns the transcript, and TdtDecoderState(from:) copied the LSTM state the same way before every recoverable decode. Reset through memset over the backing extent, fill other values through a typed pointer, and memcpy between identical layouts; the element loops remain as fallbacks. --- .../TDT/Decoder/TdtDecoderState.swift | 42 +++++++++++++ .../TDT/Decoder/TdtDecoderStateV3Tests.swift | 62 +++++++++++++++++++ .../Shared/MLArrayCacheTests.swift | 39 ++++++++++++ 3 files changed, 143 insertions(+) diff --git a/Sources/FluidAudio/ASR/Parakeet/SlidingWindow/TDT/Decoder/TdtDecoderState.swift b/Sources/FluidAudio/ASR/Parakeet/SlidingWindow/TDT/Decoder/TdtDecoderState.swift index b89de5005..d0a1fc7bc 100644 --- a/Sources/FluidAudio/ASR/Parakeet/SlidingWindow/TDT/Decoder/TdtDecoderState.swift +++ b/Sources/FluidAudio/ASR/Parakeet/SlidingWindow/TDT/Decoder/TdtDecoderState.swift @@ -96,13 +96,55 @@ public struct TdtDecoderState: Sendable { } extension MLMultiArray { + /// Fills every element with `value` through the backing storage, so padded strides are covered. + /// Zero is a single `memset`; other values fill through a typed pointer where the type allows. func resetData(to value: NSNumber) { + let filled = withUnsafeMutableBytes { bytes, _ -> Bool in + guard let base = bytes.baseAddress else { + return true + } + if value == 0 { + memset(base, 0, bytes.count) + return true + } + switch dataType { + case .float32: + bytes.bindMemory(to: Float.self).update(repeating: value.floatValue) + case .float64: + bytes.bindMemory(to: Double.self).update(repeating: value.doubleValue) + case .int32: + bytes.bindMemory(to: Int32.self).update(repeating: value.int32Value) + default: + return false + } + return true + } + if filled { + return + } for i in 0.. Bool in + source.withUnsafeBytes { origin -> Bool in + guard dataType == source.dataType, shape == source.shape, strides == source.strides, + destination.count == origin.count, let to = destination.baseAddress, + let from = origin.baseAddress + else { + return false + } + memcpy(to, from, destination.count) + return true + } + } + if copied { + return + } for i in 0.. Date: Sun, 20 Sep 2026 14:18:23 +0530 Subject: [PATCH 2/9] fix(asr): memmove for overlapping MLMultiArray copies memcpy is undefined when source and destination overlap, which zero-copy views of one allocation and a self-copy can produce. memmove keeps the bulk path correct there, a self-copy returns early, and the tests cover overlapping views, self-copy, and the padding bytes of an aligned array. --- .../TDT/Decoder/TdtDecoderState.swift | 8 +++-- .../TDT/Decoder/TdtDecoderStateV3Tests.swift | 34 +++++++++++++++++++ .../Shared/MLArrayCacheTests.swift | 5 +++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/Sources/FluidAudio/ASR/Parakeet/SlidingWindow/TDT/Decoder/TdtDecoderState.swift b/Sources/FluidAudio/ASR/Parakeet/SlidingWindow/TDT/Decoder/TdtDecoderState.swift index d0a1fc7bc..33214afc4 100644 --- a/Sources/FluidAudio/ASR/Parakeet/SlidingWindow/TDT/Decoder/TdtDecoderState.swift +++ b/Sources/FluidAudio/ASR/Parakeet/SlidingWindow/TDT/Decoder/TdtDecoderState.swift @@ -128,8 +128,12 @@ extension MLMultiArray { } /// Copies every element from `source`. Identical layouts copy the backing storage in one - /// `memcpy`; anything else goes element by element. + /// `memmove`, which stays correct when the two arrays are views of one allocation; anything else + /// goes element by element. func copyData(from source: MLMultiArray) { + if self === source { + return + } let copied = withUnsafeMutableBytes { destination, _ -> Bool in source.withUnsafeBytes { origin -> Bool in guard dataType == source.dataType, shape == source.shape, strides == source.strides, @@ -138,7 +142,7 @@ extension MLMultiArray { else { return false } - memcpy(to, from, destination.count) + memmove(to, from, destination.count) return true } } diff --git a/Tests/FluidAudioTests/ASR/Parakeet/SlidingWindow/TDT/Decoder/TdtDecoderStateV3Tests.swift b/Tests/FluidAudioTests/ASR/Parakeet/SlidingWindow/TDT/Decoder/TdtDecoderStateV3Tests.swift index c46bca83f..dd577bf58 100644 --- a/Tests/FluidAudioTests/ASR/Parakeet/SlidingWindow/TDT/Decoder/TdtDecoderStateV3Tests.swift +++ b/Tests/FluidAudioTests/ASR/Parakeet/SlidingWindow/TDT/Decoder/TdtDecoderStateV3Tests.swift @@ -276,6 +276,40 @@ final class TdtDecoderStateV3Tests: XCTestCase { } } + func testMLMultiArrayCopyDataBetweenOverlappingViews() throws { + // Two zero-copy views of one allocation, offset by 16 elements, overlap on 48 of their 64 + // elements; the copy must behave as if the source were read completely first. + let backing = try ANEMemoryUtils.createAlignedArray(shape: [1, 96], dataType: .float32) + for i in 0.. Date: Mon, 21 Sep 2026 00:46:21 +0530 Subject: [PATCH 3/9] fix(asr): bulk-fill only contiguous MLMultiArray storage withUnsafeMutableBytes reports the padded byte span, so a view built with padded strides over a tighter allocation would be overrun by a span-wide memset or copy. The bulk paths now require the span to equal count times the element size; padded layouts keep the element loop, so nothing past the last element is written. The copy goes through copyMemory, which is overlap-safe, and the doc comments state the overlap and -0.0 behavior. --- .../TDT/Decoder/TdtDecoderState.swift | 42 ++++++++------- .../TDT/Decoder/TdtDecoderStateV3Tests.swift | 53 +++++++++++++++++++ 2 files changed, 75 insertions(+), 20 deletions(-) diff --git a/Sources/FluidAudio/ASR/Parakeet/SlidingWindow/TDT/Decoder/TdtDecoderState.swift b/Sources/FluidAudio/ASR/Parakeet/SlidingWindow/TDT/Decoder/TdtDecoderState.swift index 33214afc4..41ed5988d 100644 --- a/Sources/FluidAudio/ASR/Parakeet/SlidingWindow/TDT/Decoder/TdtDecoderState.swift +++ b/Sources/FluidAudio/ASR/Parakeet/SlidingWindow/TDT/Decoder/TdtDecoderState.swift @@ -96,12 +96,17 @@ public struct TdtDecoderState: Sendable { } extension MLMultiArray { - /// Fills every element with `value` through the backing storage, so padded strides are covered. - /// Zero is a single `memset`; other values fill through a typed pointer where the type allows. + /// Fills every element with `value`. + /// + /// Contiguous storage fills in bulk: zero is one `memset` for every data type, other values + /// fill through a typed pointer for float32, float64 and int32. Padded strides and other data + /// types fill element by element, so nothing past the last element is ever written. `value` is + /// compared as an `NSNumber`, so `-0.0` takes the zero path and lands as `+0.0`. func resetData(to value: NSNumber) { + let elementSize = ANEMemoryUtils.getElementSize(for: dataType) let filled = withUnsafeMutableBytes { bytes, _ -> Bool in - guard let base = bytes.baseAddress else { - return true + guard bytes.count == count * elementSize, let base = bytes.baseAddress else { + return false } if value == 0 { memset(base, 0, bytes.count) @@ -127,27 +132,24 @@ extension MLMultiArray { } } - /// Copies every element from `source`. Identical layouts copy the backing storage in one - /// `memmove`, which stays correct when the two arrays are views of one allocation; anything else - /// goes element by element. + /// Copies every element from `source`. + /// + /// Identical contiguous layouts copy the storage in bulk; that copy is overlap-safe, so two + /// views of one allocation may overlap. Any other pair copies element by element and assumes + /// the two arrays do not share storage. func copyData(from source: MLMultiArray) { - if self === source { - return - } - let copied = withUnsafeMutableBytes { destination, _ -> Bool in - source.withUnsafeBytes { origin -> Bool in - guard dataType == source.dataType, shape == source.shape, strides == source.strides, - destination.count == origin.count, let to = destination.baseAddress, - let from = origin.baseAddress - else { + let elementSize = ANEMemoryUtils.getElementSize(for: dataType) + if dataType == source.dataType, shape == source.shape, strides == source.strides { + let copied = withUnsafeMutableBytes { destination, _ -> Bool in + guard destination.count == count * elementSize else { return false } - memmove(to, from, destination.count) + source.withUnsafeBytes { destination.copyMemory(from: $0) } return true } - } - if copied { - return + if copied { + return + } } for i in 0...allocate(capacity: elements) + defer { storage.deallocate() } + storage.initialize(repeating: 1, count: elements) + let sentinel: Float = 12345 + for i in 26...allocate(capacity: elements) + let destinationStorage = UnsafeMutablePointer.allocate(capacity: elements) + defer { + sourceStorage.deallocate() + destinationStorage.deallocate() + } + sourceStorage.initialize(repeating: 1, count: elements) + let sentinel: Float = 12345 + destinationStorage.initialize(repeating: sentinel, count: elements) + let sourceView = try MLMultiArray( + dataPointer: UnsafeMutableRawPointer(sourceStorage), shape: [2, 10], dataType: .float32, + strides: [16, 1], deallocator: nil) + let destinationView = try MLMultiArray( + dataPointer: UnsafeMutableRawPointer(destinationStorage), shape: [2, 10], dataType: .float32, + strides: [16, 1], deallocator: nil) + fillArrayWithTestData(sourceView, multiplier: 3) + + destinationView.copyData(from: sourceView) + + verifyArraysEqual(destinationView, sourceView) + for i in 26.. Date: Mon, 21 Sep 2026 00:48:56 +0530 Subject: [PATCH 4/9] refactor(shared): one MLMultiArray fill helper resetData and copyData move next to reset(to:) in Shared, and reset(to:) delegates to them, so MLArrayCache no longer reaches into the decoder for its helper. reset(to:) used to walk count contiguous slots, which skipped the last rows of a padded array; the stride-aware fill covers them. The warm-up's private vDSP fill goes the same way. --- .../TDT/Decoder/TdtDecoderState.swift | 62 ----------------- .../Shared/MLMultiArray+Extensions.swift | 68 +++++++++++++++++-- Sources/FluidAudio/Shared/ModelWarmup.swift | 22 ++---- .../Shared/MLMultiArrayExtensionsTests.swift | 22 ++++++ 4 files changed, 89 insertions(+), 85 deletions(-) create mode 100644 Tests/FluidAudioTests/Shared/MLMultiArrayExtensionsTests.swift diff --git a/Sources/FluidAudio/ASR/Parakeet/SlidingWindow/TDT/Decoder/TdtDecoderState.swift b/Sources/FluidAudio/ASR/Parakeet/SlidingWindow/TDT/Decoder/TdtDecoderState.swift index 41ed5988d..b25a65e1e 100644 --- a/Sources/FluidAudio/ASR/Parakeet/SlidingWindow/TDT/Decoder/TdtDecoderState.swift +++ b/Sources/FluidAudio/ASR/Parakeet/SlidingWindow/TDT/Decoder/TdtDecoderState.swift @@ -94,65 +94,3 @@ public struct TdtDecoderState: Sendable { // Keep LSTM states as they represent the final linguistic context } } - -extension MLMultiArray { - /// Fills every element with `value`. - /// - /// Contiguous storage fills in bulk: zero is one `memset` for every data type, other values - /// fill through a typed pointer for float32, float64 and int32. Padded strides and other data - /// types fill element by element, so nothing past the last element is ever written. `value` is - /// compared as an `NSNumber`, so `-0.0` takes the zero path and lands as `+0.0`. - func resetData(to value: NSNumber) { - let elementSize = ANEMemoryUtils.getElementSize(for: dataType) - let filled = withUnsafeMutableBytes { bytes, _ -> Bool in - guard bytes.count == count * elementSize, let base = bytes.baseAddress else { - return false - } - if value == 0 { - memset(base, 0, bytes.count) - return true - } - switch dataType { - case .float32: - bytes.bindMemory(to: Float.self).update(repeating: value.floatValue) - case .float64: - bytes.bindMemory(to: Double.self).update(repeating: value.doubleValue) - case .int32: - bytes.bindMemory(to: Int32.self).update(repeating: value.int32Value) - default: - return false - } - return true - } - if filled { - return - } - for i in 0.. Bool in - guard destination.count == count * elementSize else { - return false - } - source.withUnsafeBytes { destination.copyMemory(from: $0) } - return true - } - if copied { - return - } - } - for i in 0.. Bool in + guard bytes.count == count * elementSize, let base = bytes.baseAddress else { + return false + } + if value == 0 { + memset(base, 0, bytes.count) + return true + } + switch dataType { + case .float32: + bytes.bindMemory(to: Float.self).update(repeating: value.floatValue) + case .float64: + bytes.bindMemory(to: Double.self).update(repeating: value.doubleValue) + case .int32: + bytes.bindMemory(to: Int32.self).update(repeating: value.int32Value) + default: + return false + } + return true + } + if filled { + return + } + for i in 0.. Bool in + guard destination.count == count * elementSize else { + return false + } + source.withUnsafeBytes { destination.copyMemory(from: $0) } + return true + } + if copied { + return + } + } + for i in 0.. Date: Mon, 21 Sep 2026 00:49:14 +0530 Subject: [PATCH 5/9] test: cover padded fills and gate fill budgets in CI --- .../TDT/Decoder/TdtDecoderStateV3Tests.swift | 92 ++++++++++++++----- 1 file changed, 68 insertions(+), 24 deletions(-) diff --git a/Tests/FluidAudioTests/ASR/Parakeet/SlidingWindow/TDT/Decoder/TdtDecoderStateV3Tests.swift b/Tests/FluidAudioTests/ASR/Parakeet/SlidingWindow/TDT/Decoder/TdtDecoderStateV3Tests.swift index 4c53844a6..ff91d7719 100644 --- a/Tests/FluidAudioTests/ASR/Parakeet/SlidingWindow/TDT/Decoder/TdtDecoderStateV3Tests.swift +++ b/Tests/FluidAudioTests/ASR/Parakeet/SlidingWindow/TDT/Decoder/TdtDecoderStateV3Tests.swift @@ -188,18 +188,24 @@ final class TdtDecoderStateV3Tests: XCTestCase { array.resetData(to: 7) - for i in 0.. Date: Mon, 21 Sep 2026 00:50:06 +0530 Subject: [PATCH 6/9] perf(asr): stop zero-filling arrays returned to the cache The only getArray consumer overwrites the full extent of the preprocessor input with memcpy, and every caller pads the audio before that, so the reset on return was dead work: 240000 boxed stores, about 20 ms, on the path that returns the transcript. --- Sources/FluidAudio/Shared/MLArrayCache.swift | 4 +- .../Shared/MLArrayCacheTests.swift | 65 ------------------- 2 files changed, 2 insertions(+), 67 deletions(-) diff --git a/Sources/FluidAudio/Shared/MLArrayCache.swift b/Sources/FluidAudio/Shared/MLArrayCache.swift index b62ba4fbd..29ea5995c 100644 --- a/Sources/FluidAudio/Shared/MLArrayCache.swift +++ b/Sources/FluidAudio/Shared/MLArrayCache.swift @@ -31,7 +31,8 @@ actor MLArrayCache { return try ANEMemoryUtils.createAlignedArray(shape: shape, dataType: dataType) } - /// Return an array to the cache for reuse + /// Return an array to the cache for reuse. Its contents are kept: every consumer of `getArray` + /// overwrites the full extent before use, so clearing here would be wasted work. func returnArray(_ array: MLMultiArray) { let key = CacheKey( shape: array.shape.map { $0.intValue }, @@ -42,7 +43,6 @@ actor MLArrayCache { // Limit cache size per key if arrays.count < maxCacheSize / max(cache.count, 1) { - array.resetData(to: 0) arrays.append(array) cache[key] = arrays } diff --git a/Tests/FluidAudioTests/Shared/MLArrayCacheTests.swift b/Tests/FluidAudioTests/Shared/MLArrayCacheTests.swift index a38846dda..167285969 100644 --- a/Tests/FluidAudioTests/Shared/MLArrayCacheTests.swift +++ b/Tests/FluidAudioTests/Shared/MLArrayCacheTests.swift @@ -47,27 +47,6 @@ final class MLArrayCacheTests: XCTestCase { XCTAssertEqual(array2.dataType, .float32) } - func testReturnArrayResetsData() async throws { - let shape: [NSNumber] = [10] - let array = try await cache.getArray(shape: shape, dataType: .float32) - - // Set some values - for i in 0.. Date: Mon, 21 Sep 2026 00:50:16 +0530 Subject: [PATCH 7/9] fix(diarizer): bound zero-copy views by their padded span createZeroCopyView checked the logical element count against the source but built the view with padded strides, so a view whose innermost dimension is not a multiple of 16 could extend past the source storage. The check now uses the span the strides imply, as ANEMemoryUtils does. --- .../Shared/ANEMemoryOptimizer.swift | 15 +++++++++------ .../Shared/ANEMemoryOptimizerTests.swift | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/Sources/FluidAudio/Shared/ANEMemoryOptimizer.swift b/Sources/FluidAudio/Shared/ANEMemoryOptimizer.swift index aab43bc83..4541e473e 100644 --- a/Sources/FluidAudio/Shared/ANEMemoryOptimizer.swift +++ b/Sources/FluidAudio/Shared/ANEMemoryOptimizer.swift @@ -88,11 +88,14 @@ public final class ANEMemoryOptimizer { shape: [NSNumber], offset: Int = 0 ) throws -> MLMultiArray { - // Ensure we have enough data - let sourceElements = sourceArray.shape.map { $0.intValue }.reduce(1, *) - let viewElements = shape.map { $0.intValue }.reduce(1, *) - - guard offset + viewElements <= sourceElements else { + // The view pads its innermost stride, so its storage span can exceed its element count; + // bound the span, not the count, or a padded view would run past a tighter source. + let strides = calculateOptimalStrides(for: shape, dataType: sourceArray.dataType) + let viewSpan = shape.isEmpty ? 0 : strides[0].intValue * shape[0].intValue + let sourceSpan = + sourceArray.shape.isEmpty ? 0 : sourceArray.strides[0].intValue * sourceArray.shape[0].intValue + + guard offset + viewSpan <= sourceSpan else { throw DiarizerError.invalidArrayBounds } @@ -107,7 +110,7 @@ public final class ANEMemoryOptimizer { dataPointer: offsetPointer, shape: shape, dataType: sourceArray.dataType, - strides: calculateOptimalStrides(for: shape, dataType: sourceArray.dataType), + strides: strides, deallocator: nil // No deallocation since it's a view ) } diff --git a/Tests/FluidAudioTests/Shared/ANEMemoryOptimizerTests.swift b/Tests/FluidAudioTests/Shared/ANEMemoryOptimizerTests.swift index 0bf57ba40..af6d5f2da 100644 --- a/Tests/FluidAudioTests/Shared/ANEMemoryOptimizerTests.swift +++ b/Tests/FluidAudioTests/Shared/ANEMemoryOptimizerTests.swift @@ -193,6 +193,25 @@ final class ANEMemoryOptimizerTests: XCTestCase { // MARK: - Memory Pressure Tests + // MARK: - Zero-Copy View Bounds + + func testZeroCopyViewRejectsPaddedSpanBeyondSource() throws { + // A [10, 10] view pads its rows to 16 elements, so its storage span is 160 elements; a + // 100-element source cannot back it even though it holds 100 logical elements. + let source = try optimizer.createAlignedArray(shape: [100], dataType: .float32) + + XCTAssertThrowsError(try optimizer.createZeroCopyView(from: source, shape: [10, 10])) + } + + func testZeroCopyViewAcceptsSpanWithinSource() throws { + let source = try optimizer.createAlignedArray(shape: [1024], dataType: .float32) + + let view = try optimizer.createZeroCopyView(from: source, shape: [256], offset: 768) + + XCTAssertEqual(view.shape, [256]) + XCTAssertEqual(view.dataPointer, source.dataPointer.advanced(by: 768 * MemoryLayout.stride)) + } + func testMemoryPressureHandling() throws { // Create many buffers to simulate memory pressure var buffers: [MLMultiArray] = [] From 2d0d727af2765cd4d9957a5c21e84aad545cee74 Mon Sep 17 00:00:00 2001 From: vakharwalad23 Date: Tue, 22 Sep 2026 10:37:01 +0530 Subject: [PATCH 8/9] fix(shared): snapshot copyData fallback source The element-wise fallback read the source while writing the destination, so two views of one allocation with different layouts clobbered source elements before they were read. Read the whole source first; the bulk path for identical layouts was already overlap-safe. Test case from Nathan Roll. --- .../Shared/MLMultiArray+Extensions.swift | 7 +++--- .../TDT/Decoder/TdtDecoderStateV3Tests.swift | 23 +++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/Sources/FluidAudio/Shared/MLMultiArray+Extensions.swift b/Sources/FluidAudio/Shared/MLMultiArray+Extensions.swift index 705bfad97..a6d6c399a 100644 --- a/Sources/FluidAudio/Shared/MLMultiArray+Extensions.swift +++ b/Sources/FluidAudio/Shared/MLMultiArray+Extensions.swift @@ -46,8 +46,8 @@ extension MLMultiArray { /// Copies every element from `source`. /// /// Identical contiguous layouts copy the storage in bulk; that copy is overlap-safe, so two - /// views of one allocation may overlap. Any other pair copies element by element and assumes - /// the two arrays do not share storage. + /// views of one allocation may overlap. Any other pair reads the whole source before writing, + /// so overlapping views with different layouts stay safe too. func copyData(from source: MLMultiArray) { let elementSize = ANEMemoryUtils.getElementSize(for: dataType) if dataType == source.dataType, shape == source.shape, strides == source.strides { @@ -62,8 +62,9 @@ extension MLMultiArray { return } } + let values = (0.. Date: Tue, 22 Sep 2026 10:37:28 +0530 Subject: [PATCH 9/9] test: check row gaps in padded-view fills The padded-view sentinel tests only checked storage past the last element. A bulk write covering the logical byte count would clobber the slots between the rows and still pass, so the sentinels now cover those slots too. --- .../TDT/Decoder/TdtDecoderStateV3Tests.swift | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/Tests/FluidAudioTests/ASR/Parakeet/SlidingWindow/TDT/Decoder/TdtDecoderStateV3Tests.swift b/Tests/FluidAudioTests/ASR/Parakeet/SlidingWindow/TDT/Decoder/TdtDecoderStateV3Tests.swift index 6d622b73e..3da64d912 100644 --- a/Tests/FluidAudioTests/ASR/Parakeet/SlidingWindow/TDT/Decoder/TdtDecoderStateV3Tests.swift +++ b/Tests/FluidAudioTests/ASR/Parakeet/SlidingWindow/TDT/Decoder/TdtDecoderStateV3Tests.swift @@ -277,14 +277,16 @@ final class TdtDecoderStateV3Tests: XCTestCase { func testMLMultiArrayResetDataStaysInsideTheLogicalElements() throws { // A padded layout reports a byte span past its last element: shape [2, 10] with strides - // [16, 1] ends at element 26 while the span covers 32. Storage beyond the last element can - // belong to someone else, so the reset must not touch it. + // [16, 1] skips slots 10 to 15 between its rows and ends at element 26 while the span + // covers 32. Storage between the rows or beyond the last element can belong to someone + // else, so the reset must not touch it. let elements = 32 + let outside = Array(10..<16) + Array(26...allocate(capacity: elements) defer { storage.deallocate() } storage.initialize(repeating: 1, count: elements) let sentinel: Float = 12345 - for i in 26...allocate(capacity: elements) let destinationStorage = UnsafeMutablePointer.allocate(capacity: elements) defer { @@ -425,8 +428,8 @@ final class TdtDecoderStateV3Tests: XCTestCase { destinationView.copyData(from: sourceView) verifyArraysEqual(destinationView, sourceView) - for i in 26..