Fix memory leak when updating images.

This commit is contained in:
2020-05-21 13:52:39 +02:00
parent 11fcf0cfc2
commit da67899921
5 changed files with 179 additions and 112 deletions

View File

@@ -12,7 +12,7 @@ bin = @["randopix", "pixctrl"]
requires "nim >= 1.0.0", "gintro >= 0.5.5", "argparse >=0.10.1"
task debug, "Compile debug version":
exec "nim c -d:debug --debugger:native src/randopix.nim"
exec "nim c -d:debug --debugger:native --out:randopix src/randopix.nim"
task release, "Compile release version":
exec fmt"nim c -d:release --out:randopix-{version} src/randopix.nim"

View File

@@ -1,17 +1,28 @@
import json
const
defaultPort* = 5555 ## Default port at which the control server will run
type
OpResult* = object of RootObj ## Result object for signalling failure state across proc calls
success*: bool ## Indicating if the opration was successfull
errorMsg*: string ## Error meassge in case the operation failed
Command* = enum
cClose = "close" ## Closes the control server and exists the applicaiton
cRefresh = "refresh" ## Force refresh of the image now
cTimeout = "timeout" ## Set image timeout to a new value
CommandMessage* = object
CommandMessage* = object of RootObj
command*: Command ## Command that the application should execute
parameter*: string ## Optional parameter for the command
proc newOpResult*(): OpResult =
OpResult(success: true)
proc newOpResult*(msg: string): OpResult =
OpResult(success: false, errorMsg: msg)
proc newCommand*(c: Command, p: string = ""): CommandMessage =
CommandMessage(command: c, parameter: p)

View File

@@ -1,17 +1,23 @@
import os, sets, random, httpClient, json
import gintro/[gdkpixbuf]
import os, sets, random, httpClient, json, strformat
import gintro/[gdkpixbuf, gobject, gtk]
import commands
const
supportedExts = @[".png", ".jpg", ".jpeg"]
foxesUrl = "https://randomfox.ca/floof/"
tmpFile = "/tmp/randopix_tmp.png"
type
ProviderKind* {.pure.} = enum
Foxes = "foxes" ## Some nice foxes
Inspiro = "inspiro" ## Inspiring nonsense
File = "file" ## Images from a local path
FileOpResult* = object of OpResult
file*: string
ImageProvider* = ref object
ProviderKind* {.pure.} = enum
Foxes = "foxes" ## Some nice foxes
Inspiro = "inspiro" ## Inspiring nonsense
File = "file" ## Images from a local path
ImageProvider* = ref object of RootObj
verbose: bool
case kind: ProviderKind
of ProviderKind.Foxes, ProviderKind.Inspiro:
url: string
@@ -20,49 +26,25 @@ type
path*: string
files*: seq[string]
var client = newHttpClient() ## For loading images from the web
var
client = newHttpClient() ## For loading images from the web
verbose = true
proc downloadFox(ip: ImageProvider): Pixbuf =
## Download image from the fox API
let urlData = client.getContent(ip.url)
let info = parseJson(urlData)
let imageData = client.getContent(info["image"].getStr)
let loader = newPixbufLoader()
discard loader.write(imageData)
loader.getPixbuf()
proc reloadFileList(ip: ImageProvider) =
## Reload the file list
if ip.path == "":
return
for file in walkDirRec(ip.path):
let split = splitFile(file)
if ip.exts.contains(split.ext):
ip.files.add(file)
randomize()
shuffle(ip.files)
proc next*(ip: ImageProvider): Pixbuf =
## Return a new image from the chosen image source
case ip.kind
of ProviderKind.Foxes, ProviderKind.Inspiro:
return ip.downloadFox
of ProviderKind.File:
if ip.files.len < 1:
ip.reloadFileList
result = ip.files[0].newPixbufFromFile
ip.files.delete(0)
########################
# Constructors
########################
proc newFileProvider(path: string): ImageProvider =
## Create an image provider to access images from the local file system
randomize()
result = ImageProvider(kind: ProviderKind.File, path: path, exts: supportedExts.toHashSet)
result.reloadFileList
proc newFoxProvider(): ImageProvider = ImageProvider(kind: ProviderKind.Foxes, url: foxesUrl)
proc newFoxProvider(): ImageProvider =
## Create an image provider to access the API at "https://randomfox.ca/floof/".
ImageProvider(kind: ProviderKind.Foxes, url: foxesUrl)
proc newImageProvider*(kind: ProviderKind, filePath: string = ""): ImageProvider =
## Create a new `ImageProvider` for the API chosen with thge `kind` parameter
## Create a new `ImageProvider` for the API.
case kind
of ProviderKind.Foxes:
newFoxProvider()
@@ -70,4 +52,97 @@ proc newImageProvider*(kind: ProviderKind, filePath: string = ""): ImageProvider
# TODO
newFoxProvider()
of ProviderKind.File:
newFileProvider(filePath)
newFileProvider(filePath)
proc newFileOpResultError(msg: string): FileOpResult =
FileOpResult(success: false, errorMsg: msg)
proc newFileOpResult(file: string): FileOpResult =
FileOpResult(success: true, file: file)
########################
# Utilities
########################
proc log(msg: string) =
if verbose: echo msg
########################
# Image Provider procs
########################
proc getFox(ip: ImageProvider): FileOpResult =
## Download image from the fox API
try:
let urlData = client.getContent(ip.url)
let info = parseJson(urlData)
let imageData = client.getContent(info["image"].getStr)
let dlFile = fmt"{tmpFile}.download"
writeFile(dlFile, imageData)
return newFileOpResult(dlFile)
except JsonParsingError:
log fmt"Error while fetching from fox API: {getCurrentExceptionMsg()}"
return newFileOpResultError("Json parsing error")
except KeyError:
log fmt"No image in downloaded data: {getCurrentExceptionMsg()}"
return newFileOpResultError("No image from API")
proc getLocalFile(ip: var ImageProvider): FileOpResult =
## Provide an image from a local folder
# First, check if there are still images left to be loaded.
# If not reread all files from the path
if ip.files.len < 1:
if ip.path == "":
return newFileOpResultError("No path for image loading")
log "Reloading file list..."
for file in walkDirRec(ip.path):
let split = splitFile(file)
if ip.exts.contains(split.ext):
ip.files.add(file)
log fmt"Loaded {ip.files.len} files"
shuffle(ip.files)
# Remove the current file after
result = newFileOpResult(ip.files[0])
ip.files.delete(0)
proc getFileName(ip: var ImageProvider): FileOpResult =
## Get the temporary file name of the next file to display
case ip.kind
of ProviderKind.File:
result = ip.getLocalFile()
of ProviderKind.Foxes:
result = ip.getFox()
else:
result = newFileOpResultError("Not implemented")
########################
# Exported procs
########################
proc next*(ip: var ImageProvider, width, height: int): FileOpResult =
## Uses the image provider to get a new image ready to display.
## `width` and `height` should be the size of the window.
let op = ip.getFileName()
if not op.success: return op
var rawPixbuf = newPixbufFromFile(op.file)
# resize the pixbuf to best fit on screen
var w, h: int
if (width > height):
h = height
w = ((rawPixbuf.width * h) / rawPixbuf.height).toInt
else:
w = width
h = ((rawPixbuf.height * w) / rawPixbuf.width).toInt
var pixbuf = rawPixbuf.scaleSimple(w, h, InterpType.bilinear)
# The pixbuf is written to disk and loaded again once because
# directly setting the image from a pixbuf will leak memory
let saved = pixbuf.savev(tmpFile, "png", @[])
if not saved:
return newFileOpResultError("Error while saving temporary image")
# GTK pixbuf leaks memory when not manually decreasing reference count
pixbuf.genericGObjectUnref()
rawPixbuf.genericGObjectUnref()
newFileOpResult(tmpFile)

View File

@@ -1,8 +1,6 @@
import os, options, strformat
import gintro/[glib, gobject, gdkpixbuf]
import gintro/[glib, gobject, gdkpixbuf, gtk, gio]
import gintro/gdk except Window
import gintro/gtk except newSocket, Socket
import gintro/gio except Socket
import argparse except run
import providers, server, commands
@@ -21,7 +19,6 @@ var
args: Args ## The parsed command line args
# Widgets
window: ApplicationWindow
imageWidget: Image
label: Label
# Server vor recieving commands from external tools
serverWorker: system.Thread[void]
@@ -67,37 +64,20 @@ proc newArgs(): Option[Args] =
echo getCurrentExceptionMsg()
echo p.help
proc updateImage(): bool =
proc updateImage(image: Image): bool =
## Updates the UI with a new image
# Loading new image
try:
if (args.verbose): echo "Refreshing..."
# TODO better error signalling from providers.nim
let data = some(imageProvider.next)
if data.isNone:
label.notify "No image to display..."
return false;
## Resize image to best fit the window
var pixbuf = data.get()
var wWidth, wHeight, width, height: int
var wWidth, wHeight: int
window.getSize(wWidth, wHeight)
if (wWidth > wHeight):
height = wHeight
width = ((pixbuf.width * height) / pixbuf.height).toInt
else:
width = wWidth
height = ((pixbuf.height * width) / pixbuf.width).toInt
pixbuf = pixbuf.scaleSimple(width, height, InterpType.bilinear)
# Update the UI with the image
imageWidget.setFromPixbuf(pixbuf)
if (args.verbose):
label.notify "New image set"
let op = imageProvider.next(wWidth, wHeight)
result = op.success
if op.success:
image.setFromFile(op.file)
else:
label.notify
return true
label.notify op.errorMsg
except:
let
e = getCurrentException()
@@ -105,17 +85,19 @@ proc updateImage(): bool =
echo "Got exception ", repr(e), " with message ", msg
return false
proc forceUpdate(action: SimpleAction; parameter: Variant;) =
discard updateImage()
proc forceUpdate(action: SimpleAction; parameter: Variant; image: Image) =
discard updateImage(image)
proc timedUpdate(image: Widget): bool =
let ok = updateImage();
proc timedUpdate(image: Image): bool =
let ok = updateImage(image);
if not ok:
label.notify "Error while refreshing image, retrying..."
discard timeoutAdd(uint32(args.timeout), timedUpdate, imageWidget)
else:
label.notify
discard timeoutAdd(uint32(args.timeout), timedUpdate, image)
return false
proc checkServerChannel(parameter: string): bool =
proc checkServerChannel(image: Image): bool =
## Check the channel from the control server for incomming commands
let tried = chan.tryRecv()
@@ -125,7 +107,7 @@ proc checkServerChannel(parameter: string): bool =
case msg.command
of cRefresh:
discard updateImage()
discard updateImage(image)
of cTimeout:
let val = msg.parameter.parseInt * 1000
echo "Setting timeout to ", val
@@ -134,8 +116,8 @@ proc checkServerChannel(parameter: string): bool =
echo "Command ignored: ", msg.command
sleep(100)
result = false
discard idleAdd(checkServerChannel, parameter)
result = true
# discard idleAdd(checkServerChannel, parameter)
proc toggleFullscreen(action: SimpleAction; parameter: Variant; window: ApplicationWindow) =
## Fullscreen toggle event
@@ -158,25 +140,6 @@ proc quit(action: SimpleAction; parameter: Variant; app: Application) =
## Application quit event
cleanUp(window, app)
proc connectSignals(app: Application) =
## Connect the GTK signals to the procs
let fullscreenAction = newSimpleAction("fullscreen")
discard fullscreenAction.connect("activate", toggleFullscreen, window)
app.setAccelsForAction("win.fullscreen", "F")
window.actionMap.addAction(fullscreenAction)
let quitAction = newSimpleAction("quit")
discard quitAction.connect("activate", quit, app)
app.setAccelsForAction("win.quit", "Escape")
window.actionMap.addAction(quitAction)
let updateImageAction = newSimpleAction("update")
discard updateImageAction.connect("activate", forceUpdate)
app.setAccelsForAction("win.update", "U")
window.actionMap.addAction(updateImageAction)
window.connect("destroy", cleanUp, app)
proc appActivate(app: Application) =
# Parse arguments from the command line
let parsed = newArgs()
@@ -196,31 +159,48 @@ proc appActivate(app: Application) =
addProviderForScreen(getDefaultScreen(), provider, STYLE_PROVIDER_PRIORITY_USER)
# Create all windgets we are gonna use
var overlay = newOverlay()
imageWidget = newImage()
label = newLabel("Starting...")
label.halign = Align.`end`
label.valign = Align.`end`
overlay.addOverlay(label)
overlay.add(imageWidget)
window.add(overlay)
let container = newOverlay()
container.addOverlay(label)
window.add(container)
let image = newImage()
container.add(image)
if args.fullscreen:
window.fullscreen
app.connectSignals
## Connect the GTK signals to the procs
let fullscreenAction = newSimpleAction("fullscreen")
discard fullscreenAction.connect("activate", toggleFullscreen, window)
app.setAccelsForAction("win.fullscreen", "F")
window.actionMap.addAction(fullscreenAction)
let quitAction = newSimpleAction("quit")
discard quitAction.connect("activate", quit, app)
app.setAccelsForAction("win.quit", "Escape")
window.actionMap.addAction(quitAction)
let updateImageAction = newSimpleAction("update")
discard updateImageAction.connect("activate", forceUpdate, image)
app.setAccelsForAction("win.update", "U")
window.actionMap.addAction(updateImageAction)
window.connect("destroy", cleanUp, app)
window.showAll
discard timeoutAdd(500, timedUpdate, imageWidget)
var timerId = timeoutAdd(500, timedUpdate, image)
## open communication channel from the control server
chan.open()
## Start the server for handling incoming commands
createThread(serverWorker, runServer)
var tag = ""
discard idleAdd(checkServerChannel, tag)
discard idleAdd(checkServerChannel, image)
when isMainModule:
let app = newApplication("org.luxick.randopix")

View File

@@ -1,2 +1,3 @@
threads:on
d:ssl
d:ssl
gc:arc