1

In my project, have a data provider, which provides data in every 2 milli seconds. Following is the delegate method in which the data is getting.

func measurementUpdated(_ measurement: Double) {
    measurements.append(measurement)

    guard measurements.count >= 300 else { return }
    ecgView.measurements = Array(measurements.suffix(300))

    DispatchQueue.main.async {
        self.ecgView.setNeedsDisplay()
    }

    guard measurements.count >= 50000 else { return }

    let olderMeasurementsPrefix = measurements.count - 50000
    measurements = Array(measurements.dropFirst(olderMeasurementsPrefix))

    print("Measurement Count : \(measurements.count)")
}

What I am trying to do is that when the array has more than 50000 elements, to delete the older measurement in the first n index of Array, for which I am using the dropFirst method of Array.

But, I am getting a crash with the following message:

Fatal error: Can't form Range with upperBound < lowerBound

I think the issue due to threading, both appending and deletion might happen at the same time, since the delegate is firing in a time interval of 2 millisecond. Can you suggest me an optimized way to resolve this issue?

4
  • Please clarify what thread(s) the measurementUpdated() would be called on. Is it more than one thread, and can either be the main thread? Commented Mar 3, 2018 at 19:15
  • Both append and drop from array are written in main thread, which you can see from the method. I guess the problem is with synchronising the append and drop. Commented Mar 3, 2018 at 19:17
  • 1
    If you're always calling measurementUpdated() from the same thread (the main thread, as you've now declared), then the append will happen before the dropFirst. They can't happen at the same time. I suggest putting an assert(Thread.isMainThread) at the top to prove you're always being called from the main thread. Commented Mar 3, 2018 at 19:22
  • Also, you didn't specify which line it crashed at. Also, what is the type of measurements? I assume Array but it's possible it's not. Commented Mar 3, 2018 at 19:30

3 Answers 3

3

So to really fix this, we need to first address two of your claims:

1) You said, in effect, that measurementUpdated() would be called on the main thread (for you said both append and dropFirst would be called on main thread. You also said several times that measurementUpdated() would be called every 2ms. You do not want to be calling a method every 2ms on the main thread. You'll pile up quite a lot of them very quickly, and get many delays in their updating, as the main thread is going to have UI stuff to be doing, and that always eats up time.

So first rule: measurementUpdated() should always be called on another thread. Keep it the same thread, though.

Second rule: The entire code path from whatever collects the data to when measurementUpdated() is called must also be on a non-main thread. It can be on the thread that measurementUpdated(), but doesn't have to be.

Third rule: You do not need your UI graph to update every 2ms. The human eye cannot perceive UI change that's faster than about 150ms. Also, the device's main thread will get totally bogged down trying to re-render as frequently as every 2ms. I bet your graph UI can't even render a single pass at 2ms! So let's give your main thread a break, by only updating the graph every, say, 150ms. Measure the current time in MS and compare against the last time you updated the graph from this routine.

Fourth rule: don't change any array (or any object) in two different threads without doing a mutex lock, as they'll sometimes collide (one thread will be trying to do an operation on it while another is too). An excellent article that covers all the current swift ways of doing mutex locks is Matt Gallagher's Mutexes and closure capture in Swift. It's a great read, and has both simple and advanced solutions and their tradeoffs.

One other suggestion: You're allocating or reallocating a few arrays every 2ms. It's unnecessary, and adds undue stress on the memory pools under the hood, I'd think. I suggest not doing append and dropsFirst calls. Try rewriting such that you have a single array that holds 50,000 doubles, and never changes size. Simply change values in the array, and keep 2 indexes so that you always know where the "start" and the "end" of the data set is within the array. i.e. pretend the next array element after the last is the first array element (pretend the array loops around to the front). Then you're not churning memory at all, and it'll operate much quicker too. You can surely find Array extensions people have written to make this trivial to use. Every 150ms you can copy the data into a second pre-allocated array in the correct order for your graph UI to consume, or just pass the two indexes to your graph UI if you own your graph UI and can adjust it to accommodate.

I don't have time right now to write a code example that covers all of this (maybe someone else does), but I'll try to revisit this tomorrow. It'd actually be a lot better for you if you made a renewed stab at it yourself, and then ask us a new question (on a new StackOverflow) if you get stuck.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for the detailed explanation. Let me re write the method and I will let you know.
2

Update As @Smartcat correctly pointed this solution has the potential of causing memory issues if the main thread is not fast enough to consume the arrays in the same pace the worker thread produces them.


The problem seems to be caused by ecgView's measurements property: you are writing to it on the thread receiving the data, while the view tries to read from it on the main thread, and simultaneous accesses to the same data from multiple thread is (unfortunately) likely to generate race conditions.

In conclusion, you need to make sure that both reads and writes happen on the same thread, and can easily be achieved my moving the setter call within the async dispatch:

let ecgViewMeasurements = Array(measurements.suffix(300))

DispatchQueue.main.async {
    self.ecgView.measurements = ecgViewMeasurements
    self.ecgView.setNeedsDisplay()
}

2 Comments

Although that's a clever way to skirt the usual need for a mutex, by creating a new array each time through the loop, it runs a great risk of chewing up memory. Remember, measurementUpdated() will be called every 2ms. The main thread won't be able to keep up, will stack up many of these ecgViewMeasurements arrays awaiting each of the main async blocks to be called.
@Smartcat yes, you're right, this can lead to memory contention if the main thread is not fast enough to consume the arrays produced from the background thread. Will try to find a better solution, thanks for pointing out the flaw.
0

According to what you say, I will assume the delegate is calling the measuramentUpdate method from a concurrent thread.

If that's the case, and the problem is really related to threading, this should fix your problem:

func measurementUpdated(_ measurement: Double) {
    DispatchQueue(label: "MySerialQueue").async {
        measurements.append(measurement)

        guard measurements.count >= 300 else { return }
        ecgView.measurements = Array(measurements.suffix(300))

        DispatchQueue.main.async {
            self.ecgView.setNeedsDisplay()
        }

        guard measurements.count >= 50000 else { return }

        let olderMeasurementsPrefix = measurements.count - 50000
        measurements = Array(measurements.dropFirst(olderMeasurementsPrefix))

        print("Measurement Count : \(measurements.count)")
    }
}

This will put the code in an serial queue. This way you can ensure that this block of code will run only one at a time.

2 Comments

This will have a problem (or continue a problem) of ecgView.measurements being modified (replaced) on the new thread at the same time as it’s read on main thread. Needs a mutex lock.
@smartcat I think that will be a problem, bcos the ecg View has to update the graph in every 2 milliseconds. Could you please let me know how this can be handled

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.