1

Here's the code I'm wondering about:

final class Foo {
    
    var subscriptions = Set<AnyCancellable>()
    
    init () {
        Timer
            .publish(every: 2, on: .main, in: .default)
            .autoconnect()
            .zip(Timer.publish(every: 3, on: .main, in: .default).autoconnect())
            .sink {
                print($0)
            }
            .store(in: &subscriptions)
    }
}

This is the output it produces:

(2020-12-08 15:45:41 +0000, 2020-12-08 15:45:42 +0000)
(2020-12-08 15:45:43 +0000, 2020-12-08 15:45:45 +0000)
(2020-12-08 15:45:45 +0000, 2020-12-08 15:45:48 +0000)
(2020-12-08 15:45:47 +0000, 2020-12-08 15:45:51 +0000)

Would this code eventually crash from memory shortage? It seems like the zip operator is storing every value that it receives but can't yet publish.

jeremyabannister
  • 3,796
  • 3
  • 16
  • 25
  • I'd say your assumption is true - Zip will hold an increasing number of values of the faster timer. – New Dev Dec 08 '20 at 16:03
  • Of course, it would take a very long time to accumulate enough to crash the app. Every 6 seconds it holds an extra Date object. Not sure how large the Date object is, but if it's, say 8 bytes, then to get to 100M it would take about 900 days (if I calculated it correctly) – New Dev Dec 08 '20 at 16:09

1 Answers1

0

zip does not limit its upstream buffer size. You can prove it like this:

import Combine

let ticket = (0 ... .max).publisher
    .zip(Empty<Int, Never>(completeImmediately: false))
    .sink { print($0) }

The (0 ... .max) publisher will try to publish 263 values synchronously (that, is, before returning control to the Zip subscriber). Run this and watch the memory gauge in Xcode's Debug navigator. It will climb steadily. You probably want to kill it after a few seconds, because it will eventually use up an awful lot of memory and make your Mac unpleasant to use before finally crashing.

If you run it in Instruments for a few seconds, you'll see that all of the allocations happen in this call stack, indicating that Zip internally uses a plain old Array to buffer the incoming values.

  66.07 MB      99.8%   174      main
  64.00 MB      96.7%   45        Publisher<>.sink(receiveValue:)
  64.00 MB      96.7%   42         Publisher.subscribe<A>(_:)
  64.00 MB      96.7%   41          Publishers.Zip.receive<A>(subscriber:)
  64.00 MB      96.7%   12           Publisher.subscribe<A>(_:)
  64.00 MB      96.7%   2             Empty.receive<A>(subscriber:)
  64.00 MB      96.7%   2              AbstractZip.Side.receive(subscription:)
  64.00 MB      96.7%   2               AbstractZip.receive(subscription:index:)
  64.00 MB      96.7%   2                AbstractZip.resolvePendingDemandAndUnlock()
  64.00 MB      96.7%   2                 protocol witness for Subscription.request(_:) in conformance Publishers.Sequence<A, B>.Inner<A1, B1, C1>
  64.00 MB      96.7%   2                  Publishers.Sequence.Inner.request(_:)
  64.00 MB      96.7%   1                   AbstractZip.Side.receive(_:)
  64.00 MB      96.7%   1                    AbstractZip.receive(_:index:)
  64.00 MB      96.7%   1                     specialized Array._copyToNewBuffer(oldCount:)
  64.00 MB      96.7%   1                      specialized _ArrayBufferProtocol._forceCreateUniqueMutableBufferImpl(countForBuffer:minNewCapacity:requiredCapacity:)
  64.00 MB      96.7%   1                       swift_allocObject
  64.00 MB      96.7%   1                        swift_slowAlloc
  64.00 MB      96.7%   1                         malloc
  64.00 MB      96.7%   1                          malloc_zone_malloc
rob mayoff
  • 375,296
  • 67
  • 796
  • 848