How to use colour for text colouring without slowing down the process?
Asked Answered
I

2

8

I have found that time of the string colouring depends on how many different NSColors are used. In code below if I use only one colour for the three cases then the text colouring process is 3 times faster than in the case when three different colours are used for these three cases, each colour for each case. Why ? Is there a way not to slow down the colouring for three different colours ?

for i in 0..<arrayOfNSRangesForA.count
{
    textFromStorage.addAttribute(NSForegroundColorAttributeName, value: NSColor.green, range: arrayOfNSRangesForA[i])
}

for i in 0..<arrayOfNSRangesForT.count
{
   textFromStorage.addAttribute(NSForegroundColorAttributeName, value: NSColor.green, range: arrayOfNSRangesForT[i])
}

for i in 0..<arrayOfNSRangesForC.count
{
    textFromStorage.addAttribute(NSForegroundColorAttributeName, value: NSColor.green, range: arrayOfNSRangesForC[i])
}

Update I have found one more BAD thing. When I changed colouring from NSForegroundColorAttributeNameto NSBackgroundColorAttributeName the running time has increased significantly - 10 times. For 20 000 characters, it was for one colour, for NSForegroundColorAttributeName- 1 sec, for NSBackgroundColorAttributeName - 10 sec; if three colours - 3 and 30 sec accordingly. For me it is very bad feature of Swift !!! It is not possible to do normal work with DNA (ATGC sequence) colouring, since the length of DNA is thousands of A,T,G,C characters!

Update In comments I have a suggestion to colour only visible part of text. I have tried this approach and it is much worse even for shorter text in comparison with what I did in standard way. So, I had NSRange of text for visible part of text, and did colouring on fly while scrolling by using notification when scrolling is on. It is a bad way.

Irreligious answered 31/1, 2017 at 16:47 Comment(13)
Depending on the specific ranges that the color is applied to, they may coalesce to one run of all the same attributes. Or, at least, fewer runs than when you use difference colors. There's a separate draw operation per run, with overhead for each operation. You can use enumerateAttributes(in:options:using:) to see how many runs there are for each case.Telegenic
@Ken Thomases I have tested a string around 180 000 characters composed by A, G, T and C characters in different amounts, actually it is DNA sequence. First I determine the ranges for each A , G, T and C. So, I have 4 arrays of ranges and then I colour A , G, T and C characters.Irreligious
@Ken Thomases, since the arrays for each C,G,A and T are the same, for one color or for 3 color, the coalesce of ranges can take place only at time of colouring process, and then it means that somehow colouring for each loop depends on other loops ??Irreligious
The loops are not "coloring" anything. They are simply setting attributes on ranges of characters. It's not documented how exactly (or if) NSMutableAttributedString coalesces attribute runs. It's possible that each time attributes are changed for a range, that it checks if the immediately preceding or following characters share the new attribute set. If they do, it could coalesce them into one run. Or it could use some other approach.Telegenic
@Ken Thomases Whatever the mechanism of colouring is, the question is how to avoid the slow down effect in colouring ? Is there hope for this or not ?Irreligious
Have you confirmed that, in the all-the-same-color case, that the number of attribute runs is significantly less than in the multi-color case?Telegenic
@Ken Thomases I did not make the test, but I did one check which implies that the colouring itself, not a process connected to the ranges, is responsible. When I changed the colouring scheme from NSForegroundColorAttributeName to NSBackgroundColorAttributeName, (rest of code the same) colouring time has increased significantly. Fro 20 000 characters from 3 sec to 30 sec.Irreligious
NSAttributedString is intended for managing formatted text for display on the screen. It is not designed to be good at manipulating DNA sequences. Are you using this class to display enormous DNA strings?Crozier
@Dave Weston All manipulations with DNA sequences are done with text as String, and it is working well, not NSAttributedString. Only to colour the final resulting String I am using NSAttributedString, exactly for purpose to display it on the screen in colour.Irreligious
@Dave Weston Is there other way to display a long String in colour, rather than converting it to NSAttributedString ?Irreligious
If NSAttributedString is not performant enough for you, you may be able to drop down a level to the Core Text framework. Or, another option might be to build the NSAttributedString in a background thread, so it doesn't slow down the main thread. Or color just the visible portion of the string at a time. It depends on your situation.Crozier
@Dave Weston Could you advise a source with info how the colouring of the only visible portion of text in NSTextView can be done ? Also, in this case, how scrolling is working ? Now scrolling is freezing from time to time.Irreligious
Have you read the Text Programming Guide? developer.apple.com/library/content/documentation/… There is a section on using Text Kit. The guide is geared at UIKit developers, but since Text Kit was ported from Cocoa, it should be pretty close to the same.Crozier
M
2

The biggest obstacle is laying out all these attributed characters in the text view. Colorize the DNA sequence takes minimal amount of time. Instead of writing your own layout manager or text storage class, you can adopt a divide-and-conquer approach by colorizing the text view in chunks at a time:

@IBOutlet var textView: NSTextView!
var dnaSequence: String!
var attributedDNASequence: NSAttributedString!

@IBAction func colorize(_ sender: Any) {
    self.dnaSequence = "ACGT" // your plaintext DNA sequence
    self.attributedDNASequence = self.makeAttributedDNASequence()

    // Rendering long string with the same attributes throughout is extremely fast
    self.textView.textStorage?.setAttributedString(NSAttributedString(string: dnaSequence))

    let step = 10_000   // colorize 10k characters at a time
    let delay = 0.2     // delay between each render
    for (i, location) in stride(from: 0, to: self.dnaSequence.characters.count, by: step).enumerated() {
        let length = min(step, self.dnaSequence.characters.count - location)
        let range = NSMakeRange(location, length)

        // Since we are modifying the textStorage of a GUI object (NSTextView)
        // we should do it on the main thread
        DispatchQueue.main.asyncAfter(deadline: .now() + (delay * Double(i))) {
            let subtext = self.attributedDNASequence.attributedSubstring(from: range)

            print("Replacing text in range \(location) to \(location + length)")
            self.textView.textStorage?.replaceCharacters(in: range, with: subtext)
        }
    }
}


// MARK: -
var colorA = NSColor.red
var colorC = NSColor.green
var colorG = NSColor.blue
var colorT = NSColor.black

func makeAttributedDNASequence() -> NSAttributedString {
    let attributedText = NSMutableAttributedString(string: dnaSequence)
    var index = dnaSequence.startIndex
    var color: NSColor!

    for i in 0..<dnaSequence.characters.count {
        switch dnaSequence[index] {
        case "A":
            color = colorA
        case "C":
            color = colorC
        case "G":
            color = colorG
        case "T":
            color = colorT
        default:
            color = NSColor.black
        }

        attributedText.addAttribute(NSForegroundColorAttributeName, value: color, range: NSMakeRange(i,1))
        index = dnaSequence.index(after: index)
    }

    return attributedText
}

The trick is to make the application as responsive as possible so the user is unaware that things are still being done in the background. With a small delay (<= 0.3 second) I couldn't scroll my mouse fast enough to reach the end of text view before everything has been colorized (100k characters).

On a 100k-character test, it took 0.7 seconds to until the colorized string first appeared inside the text view instead of the 7 seconds if I did everything at once.

Mestas answered 9/2, 2017 at 1:1 Comment(3)
Thanks for the answer! No, I placed setAttributedString like in your code. Only differences are the first finding of NSRanges for each A, T, C and then colouring, not using switch . I do two strings alignment and then colouring of combined string. So, I have wrote a bit wrong about string length. I have used about 20 000 for each, so the combined string is about 40 000, and it is around 3 sec. For your code (I have checked) the results are similar. I have up voted your answer since it is another way to colour the DNA string. Unfortunately it is not solving the problem.Irreligious
On deeper look, creating the attributed string was very fast (<1s for 100k-character string). It's laying them out the NSTextView that's slow. I'll take a deeper dive into NSTextView and update my answer after work.Mestas
Thanks again! I accept your answer as a solution and will adapt your code to my app.Irreligious
A
-1

Have you tried using a ciColor instead of an attribute? ciColors can be used with text, images and backgrounds.

You can try like this:

txtField.textColor?.ciColor.red
Antipole answered 8/2, 2017 at 6:23 Comment(1)
I cannot find how can I use ciColor in NSTextView. What I have red is - "You use CIColor objects in conjunction with other Core Image classes, such as CIFilter, CIContext,and CIImage"Irreligious

© 2022 - 2024 — McMap. All rights reserved.