How I Made MagicToy Clipboard 100 times faster

MagicToy‘s clipboard supports copying of text, formatted code, images, etc.

When users copy a large number of images, the clipboard will become very laggy.

MagicToy is dedicated to providing users with useful tools, and a laggy clipboard is a million miles away from being “useful”.

Taking the opportunity to add the Paste directly into another app feature to MagicToy, I decided to fix this performance problem. With very minor modifications, MagicToy’s clipboard loaded ten 5K images with almost 100 times faster (from 1300ms to 15ms).

In order to show you more clearly how I fixed the problem, I better explain the gui, business logic and class definitions of MagicToy’s clipboard first.

What MagicToy’s Clipboard Looks Like

MagicToy wanted to provide users with a simple, yet secure, clipboard. This is what it looks like:

If you don’t like the way it looks by default, you can choose your favorite one among more than a dozen beautiful themes. You can even customize the colors, fonts, sizes, borders, etc. of its various parts.

https://www.youtube.com/watch?v=9Q-vgpyLU0Y

But most importantly, it’s a secure clipboard. By setting the maximum retention time for your clipboard items, it will help you reduce the duration your sensitive information is exposed, thus protecting your passwords, accounts, wallet keys, or mnemonics and more.

In addition, it supports previewing codes, text, images, notes, search, favorites.

And what causes the clipboard to slow down are those features about pictures.

Data structures

In order to introduce the topic of this article more efficiently, I will skip some details and keep only the code that is sufficiently descriptive.

@Model
public class ClipboardItem: Identifiable, Equatable, ObservableObject {
    public var uid = UUID()
    @Attribute(.externalStorage)
    private var data: [Data?]
}

As you can see above, MagicToy uses SwiftData to store the clipboard data. SwiftData simplifies the work needed to work with sqlite, and is much easier than CoreData (it actually took me half a day to learn Swift and SwiftUI, I had tried CoreData but didn’t make it work at last).

Then, a List is used to query and display these clipboard items:

struct ClipboardView: View {
    @Environment(.modelContext) private var modelContext
    @State var hoveringItem: ClipboardItem?

    @Query
    private var items: [ClipboardItem]
    var body: some View {
      List($items) { item in
         ...
      }
    }
}

Enable sql debug in Xcode

To be able to see when the data was fetched, we should enable sql debug in Xcode here:

Problems with MagicToy

By looking at the sql log output from Xcode’s console, I found two problems:

  1. a sql query occurs when the mouse is hovered over the clipboard list.
  2. when querying a huge image, the execution time of a single sql query is very slow, maybe 100ms or so.

So what I need to do is

  1. reduce the number of queries that are not necessary, such as hovering the mouse over the list, this scenario does not require a query at all. I need to figure out why the re-rendering is happening.
  2. compress the image data saved to database so we can reduce the time it takes to read the data

Fix it.

Reduce unnecessary queries

This is the simplest step.

When the mouse hovers over a clipboard item, a re-rendering of the entire ClipboardView occurs, and the culprit is the @State var hoveringItem.

By moving the hovering logic from the List, to each item in the List, the problem can be solved quite easily.

Optimize data structure and reduce data load time with thumbnails

The only time we need to provide the user with the original image is when the user wants to paste it. For other scenarios, such as showing a thumbnail in clipboard entries or previewing them, it is sufficient to use multiple thumbnails of varying sizes.

When the program starts and the clipboard loads the data, only the thumbnails need to be loaded, and when the user pastes, then the original image is read, which can greatly reduce the data loading time.

Based on this idea, I need to implement two functions:

  1. generate thumbnails
  2. separate the data of thumbnail and original image into different tables.

Generate Thumbnail

There is not much to say about this part, I can share the compression function I used:

extension NSImage {
    func resize(_ to: CGSize, isPixels: Bool = false) -> NSImage {

        var toSize = to
        let screenScale: CGFloat = NSScreen.main?.backingScaleFactor ?? 1.0

        if isPixels {

            toSize.width = to.width / screenScale
            toSize.height = to.height / screenScale
        }

        let toRect = NSRect(x: 0, y: 0, width: toSize.width, height: toSize.height)
        let fromRect =  NSRect(x: 0, y: 0, width: size.width, height: size.height)

        let newImage = NSImage(size: toRect.size)
        newImage.lockFocus()
        draw(in: toRect, from: fromRect, operation: NSCompositingOperation.copy, fraction: 1.0)
        newImage.unlockFocus()

        return newImage
    }
}

New data structure

This is the new class definition:

@Model
public class ClipboardItemV2: Identifiable, Equatable, ObservableObject {
    public var uid = UUID()
    @Attribute(.externalStorage)
    private var thumbnail: [Data?]
}

@Model
public class ClipboardRawData: Identifiable, Equatable, ObservableObject {
    public var uid = UUID()
    @Attribute(.externalStorage)
    private var data: [Data?]
}

I also need change to thumbnails where I used raw data before, and only load the uncompressed raw data when I paste.

With these changes, the MagicToy became faster, even without a memory cache.

Before:

CoreData: annotation: total fetch execution time: 0.0750s for 4 rows.

Now:

CoreData: annotation: total fetch execution time: 0.0008s for 4 rows.

750➗8 = 93.75! Wow, that’s some optimization!

Want to get MagicToy? You can try it for free by clicking on the icon below.

MagicToy on AppStore

Leave a Comment