GithubHelp home page GithubHelp logo

Comments (9)

tannewt avatar tannewt commented on June 2, 2024

Is the core problem with the current implementation the overhead of the TileGrid class? How does this change as the text gets larger? The TG model saves glyph bitmap duplication at the expense of the TG fixed overhead and the cost of storing it in a group.

Note, I wouldn't include the current background bitmap in the calculations because it's easy to optimize on it's own.

from adafruit_circuitpython_display_text.

kmatch98 avatar kmatch98 commented on June 2, 2024

Short answer to your question:

Conclusion: label.py uses ~100 bytes per character for a 16pt font.


Here are the details

Per your questions, I did a trial between label.py and my TextMap library (https://github.com/kmatch98/CircuitPython_textMap).

For this trial, I used a string (659 characters long). I printed one additional character during each loop and then measured the memory after each 10 characters are added. I compared two fonts (Helvetica Bold 16 and Vera Sans Roman 24).

For your reference, my TextMap example creates a bitmap the size of the screen (320x240 in this case) and copies each glyph's bits into the bitmap.

I measured:

  • Memory available before starting the loop of printing the characters (to estimate "overhead"), using gc.collect() then gc.mem_free()
  • Memory reduction after each character is added (bytes per added character)

Sample text output from label.py usage with Helvetica Bold 16:

After display.show(myGroup), just before loop start  Memory free: 85232
charCount: 10, Running Memory free: 84208, mem usage in Loop: 1024, per character mem: 93.0909
charCount: 20, Running Memory free: 82944, mem usage in Loop: 2288, per character mem: 108.952
charCount: 30, Running Memory free: 81856, mem usage in Loop: 3376, per character mem: 108.903
charCount: 40, Running Memory free: 80656, mem usage in Loop: 4576, per character mem: 111.61
charCount: 50, Running Memory free: 79536, mem usage in Loop: 5696, per character mem: 111.686
charCount: 60, Running Memory free: 78368, mem usage in Loop: 6864, per character mem: 112.525
charCount: 70, Running Memory free: 77232, mem usage in Loop: 8000, per character mem: 112.676

Sample text output from TextMap usage with Helvetica Bold 16:

After display.show(myGroup), just before loop start  Memory free: 86192
charCount: 10, Running Memory free: 86320, mem usage in Loop: -128, per character mem: -11.6364
charCount: 20, Running Memory free: 86192, mem usage in Loop: 0, per character mem: 0.0
charCount: 30, Running Memory free: 86192, mem usage in Loop: 0, per character mem: 0.0
charCount: 40, Running Memory free: 86192, mem usage in Loop: 0, per character mem: 0.0
charCount: 50, Running Memory free: 86192, mem usage in Loop: 0, per character mem: 0.0

Experimental setup:

  • Itsy Bitsy NRF52840
  • ILI9341 Display 320x240

Tabular Results:

image

Summary of results:

  • For label.py, each added character increases memory usage by 110 to 125 bytes per character (dependent upon font size).
  • For a large string (659 characters), the label.py has the largest overhead.
  • Changing max_glyphs of 659 to 30 reduces the memory overhead by 5 kB, and then label.py has less overhead.

Conclusion: Using a lot of text at ~100 bytes per character eats up memory in a hurry.

from adafruit_circuitpython_display_text.

tannewt avatar tannewt commented on June 2, 2024

@kmatch98 Thank you for the empirical numbers!

I think the textmap numbers are a bit misleading though because it's a fixed overhead of 320 x 240 // 8 = 9600 bytes which means label is a better choice for strings of 9600 // 125 = 76 characters or less.

We can raise this number by reducing the overhead of each tilegrid. I think the cost of each character now is:

  • 4 bytes for the Group entry pointer.
  • the size of the displayio_tilegrid_obj_t
  • the size of the glyph's bitmap

Only the first two bullets apply when a duplicate character is used.

I think we should add a TextArea class to this library (in a different file) that takes the approach you suggest. It can optimize for multi-line text areas (aka paragraphs) while label can focus on short text, usually in a single line.

from adafruit_circuitpython_display_text.

kmatch98 avatar kmatch98 commented on June 2, 2024

I reviewed my code and decided to eliminate any difference due to the import statements. So now the import statements are consistent for all tests. Here is chart with the updated overhead:
image

@tannewt To check your comment about the third bullet, reran the code with a string with just repeated "M" in it.

I see the same ~100 byte incremental memory usage when each character is added, even thought it is a duplicated glyph. Am I missing something?

Please note that I load the glyphs before entering the loop:

glyphs = b'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-,.:?! '
font.load_glyphs(glyphs)`

As for overhead, the updated numbers show that label.py overhead is 5-10kB less, as you said.

Raw data and Test Code for label.py with "M" string

Here is the output result with the repeated "M" string using label.py and Helvetica 16:

code.py output:
Starting the display...
spi.frequency: 32000000
Display is started
loading fonts...
loading glyphs...
Glyphs are loaded.
Fonts completed loading.
After creating Group,  Memory free: 91520
myString: Full Screen Size: This is a stationary box, not a stationery box. Full Screen Size: This is a stationary box, not a stationery box. Full Screen Size: This is a stationary box, not a stationery box. Full Screen Size: This is a stationary box, not a stationery box. Full Screen Size: This is a stationary box, not a stationery box. Full Screen Size: This is a stationary box, not a stationery box. Full Screen Size: This is a stationary box, not a stationery box. Full Screen Size: This is a stationary box, not a stationery box. Full Screen Size: This is a stationary box, not a stationery box. Full Screen Size: This is a stationary box, not a stationery box.
string length: 659
After display.show(myGroup), just before loop start  Memory free: 85808
charCount: 10, Running Memory free: 84160, mem usage in Loop: 1648, per character mem: 149.818
charCount: 20, Running Memory free: 82912, mem usage in Loop: 2896, per character mem: 137.905
charCount: 30, Running Memory free: 81824, mem usage in Loop: 3984, per character mem: 128.516
charCount: 40, Running Memory free: 80640, mem usage in Loop: 5168, per character mem: 126.049
charCount: 50, Running Memory free: 79536, mem usage in Loop: 6272, per character mem: 122.98
charCount: 60, Running Memory free: 78368, mem usage in Loop: 7440, per character mem: 121.967
charCount: 70, Running Memory free: 77248, mem usage in Loop: 8560, per character mem: 120.563
charCount: 80, Running Memory free: 76096, mem usage in Loop: 9712, per character mem: 119.901
charCount: 90, Running Memory free: 74976, mem usage in Loop: 10832, per character mem: 119.033
charCount: 100, Running Memory free: 73872, mem usage in Loop: 11936, per character mem: 118.178
charCount: 110, Running Memory free: 72704, mem usage in Loop: 13104, per character mem: 118.054
charCount: 120, Running Memory free: 71584, mem usage in Loop: 14224, per character mem: 117.554
charCount: 130, Running Memory free: 70480, mem usage in Loop: 15328, per character mem: 117.008
charCount: 140, Running Memory free: 69312, mem usage in Loop: 16496, per character mem: 116.993
charCount: 150, Running Memory free: 68208, mem usage in Loop: 17600, per character mem: 116.556

For reference, my full code for memory usage testing of label.py with the "M" string is shown below:

# Sample code using the textMap library and the "textBox" wrapper class
# Creates four textBox instances
# Inserts each textBox into a tileGrid group
# Writes text into the box one character at a time
# Moves the position of the textBox around the display
# Clears each textBox after the full string is written (even if the text is outside of the box)

import textmap
from textmap import textBox

import board
import displayio
import time
import terminalio
import fontio
import sys
import busio
#from adafruit_st7789 import ST7789
from adafruit_ili9341 import ILI9341
from adafruit_display_text import label

#  Setup the SPI display

print('Starting the display...') # goes to serial only
displayio.release_displays()


spi = board.SPI()
tft_cs = board.D9 # arbitrary, pin not used
tft_dc = board.D10
tft_backlight = board.D12
tft_reset=board.D11

while not spi.try_lock():
    spi.configure(baudrate=32000000)
    pass
spi.unlock()

display_bus = displayio.FourWire(
    spi,
    command=tft_dc,
    chip_select=tft_cs,
    reset=tft_reset,
    baudrate=32000000,
    polarity=1,
    phase=1,
)

print('spi.frequency: {}'.format(spi.frequency))

DISPLAY_WIDTH=320
DISPLAY_HEIGHT=240

#display = ST7789(display_bus, width=240, height=240, rotation=0, rowstart=80, colstart=0)
display = ILI9341(display_bus, width=DISPLAY_WIDTH, height=DISPLAY_HEIGHT, rotation=180, auto_refresh=True)

display.show(None)

print ('Display is started')


# load all the fonts
print('loading fonts...')

import terminalio


fontList = []
fontHeight = []

##### the BuiltinFont terminalio.FONT has a different return strategy for get_glyphs and 
# is currently not handled by these functions.
#fontList.append(terminalio.FONT)
#fontHeight = [10] # somehow the terminalio.FONT needs to be adjusted to 10

# Load some proportional fonts
fontFiles =   [
            'fonts/Helvetica-Bold-16.bdf',
            'fonts/BitstreamVeraSans-Roman-24.bdf', # Header2
            'fonts/BitstreamVeraSans-Roman-16.bdf', # mainText
            ]

from adafruit_bitmap_font import bitmap_font

for i, fontFile in enumerate(fontFiles):
    thisFont = bitmap_font.load_font(fontFile) 
    fontList.append(thisFont)
    fontHeight.append( thisFont.get_glyph(ord("M")).height ) 

preloadTheGlyphs= True # set this to True if you want to preload the font glyphs into memory
    # preloading the glyphs will help speed up the rendering of text but will use more RAM

if preloadTheGlyphs:

    # identify the glyphs to load into memory -> increases rendering speed
    glyphs = b'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-,.:?! '

    print('loading glyphs...')
    for font in fontList:
        font.load_glyphs(glyphs)
    print('Glyphs are loaded.')

print('Fonts completed loading.')

# create group 
import gc

gc.collect()
myGroup = displayio.Group( max_size=1 ) # Create a group for displaying
tileGridList=[] # list of tileGrids
print( 'After creating Group,  Memory free: {}'.format(gc.mem_free()) )

gc.collect()

myString=('Full Screen Size: This is a stationary box, not a stationery box. Full Screen Size: This is a stationary box, not a stationery box. Full Screen Size: This is a stationary box, not a stationery box. Full Screen Size: This is a stationary box, not a stationery box. Full Screen Size: This is a stationary box, not a stationery box. Full Screen Size: This is a stationary box, not a stationery box. Full Screen Size: This is a stationary box, not a stationery box. Full Screen Size: This is a stationary box, not a stationery box. Full Screen Size: This is a stationary box, not a stationery box. Full Screen Size: This is a stationary box, not a stationery box.')
#myString=('MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM')
print('myString: {}'.format(myString))
print('string length: {}'.format(len(myString)))

text_label = label.Label(font=fontList[0], text="", color=0xFFFFFF, max_glyphs=len(myString))
myGroup.append(text_label)

display.show(myGroup)

charCount=0

gc.collect()
memBeforeLoop=gc.mem_free()
print('After display.show(myGroup), just before loop start  Memory free: {}'.format(memBeforeLoop) )

while True:

    # Add characters one at a time.

    if charCount >= len(myString):
        charCount=0
    text_label.text=myString[0:charCount] # add a character

    
    charCount += 1

    # Print the memory availability every 10 movements.
    if charCount % 10 == 0:
        gc.collect()
        currentMem=gc.mem_free()
        print( 'charCount: {}, Running Memory free: {}, mem usage in Loop: {}, per character mem: {}'.format(charCount, currentMem, memBeforeLoop-currentMem, (memBeforeLoop-currentMem)/(charCount+1)) )

from adafruit_circuitpython_display_text.

kmatch98 avatar kmatch98 commented on June 2, 2024

To keep from getting lost in the empirical measurements:

Once we better define what the problem is, I want to make sure we have an agreed strategy before going any further.

I see two approaches to consider:

  1. Make a drop-in replacement for label.py, only difference is that it's in a bitmap (and less mutable on the fly). This is the suggestion that was discussed in the call on July 20th.
  2. Make a library that is good for rendering larger areas of text, possibly including other features (e.g. word wrapping, cursor control like a terminal)

Based on what I have seen the current label.py uses an unexpectedly large amount of memory even for small amounts of text (> 76 chars?) and is causing people problems. If that's not really a problem then path #2 is probably is a better approach, to focus on applications specifically that are text-heavy. However, text-heavy applications may prefer "mutable" structures so we'll have to consider how to respond to that if path #2 is preferred.

Y'all's feedback is welcome, requested and appreciated! @FoamyGuy @tannewt

from adafruit_circuitpython_display_text.

tannewt avatar tannewt commented on June 2, 2024

I see the same ~100 byte incremental memory usage when each character is added, even thought it is a duplicated glyph. Am I missing something?

Nope! I didn't realize you were preloading the glyphs.

I see two approaches to consider:

  1. Make a drop-in replacement for label.py, only difference is that it's in a bitmap (and less mutable on the fly). This is the suggestion that was discussed in the call on July 20th.

  2. Make a library that is good for rendering larger areas of text, possibly including other features (e.g. word wrapping, cursor control like a terminal)

I would do more 2 than 1. To me, a label is a small amount of text. Hundreds of characters is not a label but rather a paragraph or text area. I don't think it needs to be in a different library though. It should just be a different module (aka file) in this library.

Based on what I have seen the current label.py uses an unexpectedly large amount of memory even for small amounts of text (> 76 chars?) and is causing people problems. If that's not really a problem then path #2 is probably is a better approach, to focus on applications specifically that are text-heavy. However, text-heavy applications may prefer "mutable" structures so we'll have to consider how to respond to that if path #2 is preferred.

I agree that 100 bytes per character is a lot. We should take a look at TileGrid to see if we can reduce that overhead. I don't think that is the primary concern of this investigation though because the memory errors people hit are due to large consecutive allocations because of the group containing many characters. 100 bytes per TG is a lot but at least they are separate memory chunks.

I'd propose adding a TextArea class that takes rectangular bounds and allocates a bitmap for the bounds once up front. Then, text can be laid out into it repeatedly without additional allocations.

Y'all's feedback is welcome, requested and appreciated! @FoamyGuy @tannewt

Thank you for your in depth thoughts on this!

from adafruit_circuitpython_display_text.

kmatch98 avatar kmatch98 commented on June 2, 2024

Thanks @tannewt. I remain suspicious of TileGrid, it seems like the overhead it excessive but I can't understand the code well enough to point to the issue.

After good discussion with @FoamyGuy this morning I decided to start with the direct drop-in replacement bitmap_label (type #1 above).

Once that is done, I'll start on #2. I've already been down this road with TextMap, so those features will be my starting scope. Main scope is to decide what features to bring from recent updates to label.py.

But before I turn to #2 , I'd like elicit feedback on the top features desired. I'm not sure the best forum to do that. I suspect the easiest method is to create something and then people will say "I wish..." The In-the-Weeds time is helpful, but I'm unsure if we can collect specific text-realted feedback there.

I am totally new to this kind of large-scale collaborative development, so I'm open to your suggestions how to scope a new add-on file to the display_text library.

from adafruit_circuitpython_display_text.

tannewt avatar tannewt commented on June 2, 2024

I think the first place to look for overhead is here: https://github.com/adafruit/circuitpython/blob/main/shared-module/displayio/TileGrid.h#L37

Most of it could be simplified for the single tile case and we could therefore split much of it into a separate sub-allocation when more than one tile is used.

from adafruit_circuitpython_display_text.

kmatch98 avatar kmatch98 commented on June 2, 2024

Closed with: #79

from adafruit_circuitpython_display_text.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.