Assets: the SQLite store, FSAssets, and keeping the store small
Every texture, mesh, sound, animation, script text, notecard and object a world uses is an asset: an immutable blob with a UUID. This chapter is about where a Paracosm keeps them, when OpenSim's file-system asset service (FSAssets) would be worth it, and how to keep the store from filling with copies nobody uses.
Where assets live now
- The store:
bin/Asset.db, tableassets(UUID,Name,Description,Type,Local,Temporary,asset_flags,CreatorID,Data). OpenSim's standardAssetServiceover SQLite (config-include/storage/SQLiteStandalone.ini:[AssetService] ConnectionString = "URI=file:Asset.db,version=3"). One row per UUID, the whole blob in the row. Nothing is shared between rows: the same bytes uploaded twice are stored twice. - The cache:
bin/assetcache/, Flotsam (config-include/FlotsamCache.ini, copied from upstream's example bymake_config.pyand never overwritten after, so edit it inbin/). A cache in front of the store, not a second store: deleting it loses nothing. - Backups copy
Asset.dbwith the other databases (tools/backup.py, backup.md).
What this house's store holds, measured read-only on a backup copy (2026-09-27, backups/...-100913/db/Asset.db,
1.07 GB):
| type | rows | bytes | of which the same bytes as another row |
|---|---|---|---|
| texture | 1,487 | 652,072,297 | 491,382,256 |
| mesh | 1,721 | 337,567,565 | 218,730,838 |
| script | 19,787 | 27,026,183 | 7,679,444 |
| sound | 469 | 20,883,362 | 9,548,979 |
| everything | 25,945 | 1,051,423,366 | 728,705,641 (69.3%) |
Two kinds of waste, then: 16,741 script rows nothing references any more (WorldBridge's old set_script leak,
below), and copies: 25,945 rows hold only 12,881 distinct contents, mostly textures and meshes uploaded again
under a new UUID each time a builder re-ran.
FSAssets: what it is
OpenSim.Services.FSAssetService.dll:FSAssetConnector (OpenSim/Services/FSAssetService) keeps each asset's data
as a gzip-compressed file named by the SHA-256 of its content, under BaseDirectory in a 00/00/00 tree, and keeps
only the metadata (UUID, name, type, the hash) in a database table (fsassets). Identical content uploaded under
many UUIDs is one file on disk: on this house's numbers the 1.05 GB of data would be about 0.32 GB before
compression. New assets are written through a spool directory by a background writer thread.
It cannot run on a SQLite-only Paracosm. Its metadata table goes through an IFSAssetDataPlugin, and
OpenSimulator 0.9.3.0 has one for MySQL (MySQLFSAssetData) and one for PostgreSQL only. With
OpenSim.Data.SQLite.dll as its storage provider it throws "Could not find a storage interface in the module" and
the region's asset connector fails to start, which stops OpenSim. So FSAssets means running MySQL/MariaDB (or
PostgreSQL) for at least its table. No SQLite FSAssets plugin has been written here.
When to switch
Stay on the SQLite store while:
- the world is one region on one machine, and
Asset.dbis a few GB or less (SQLite handles far more; the cost is one large file to back up and copy); - the growth comes from the two leaks above, which are cheaper to stop at the source (below).
Consider FSAssets when:
- you are moving to MySQL/MariaDB anyway (several regions, a grid, ROBUST);
- the store reaches tens of GB and much of it is repeated content;
- you want the asset data as plain files (per-file backup and rsync, the store on a separate disk).
The config, for an install that has MySQL
Untested here. From Robust.ini.example's [AssetService] block (the grid version) and FSAssetConnector's
constructor. On a standalone the region's asset connector loads LocalGridAssetService
(RegionAssetConnectorModule), so that is the key to change, in StandaloneCommon.ini (an overlay line in
config/StandaloneCommon-overlay.ini):
[AssetService]
LocalServiceModule = "OpenSim.Services.FSAssetService.dll:FSAssetConnector"
LocalGridAssetService = "OpenSim.Services.FSAssetService.dll:FSAssetConnector"
; data files, and the spool for new ones: on the same filesystem
BaseDirectory = "./fsassets/data"
SpoolDirectory = "./fsassets/tmp"
; its metadata table: required, and it must be MySQL or PostgreSQL
StorageProvider = "OpenSim.Data.MySQL.dll"
ConnectionString = "Data Source=localhost;Database=paracosm;User ID=paracosm;Password=...;Old Guids=true;"
; Realm = "fsassets" ; the table
; DaysBetweenAccessTimeUpdates = 30 ; fewer writes for often-read assets (default 0: every read)
; ShowConsoleStats = true ; a read-stats line every minute (the default)
; UseOsgridFormat = false ; 000/000 folders instead of 00/00/00
DefaultAssetLoader and AssetLoaderArgs (the OpenSim Library's assets) are read by FSAssets too; the standalone
already sets them in [AssetService]. The service adds console commands: show assets, show digest <id>,
delete asset <id>, and import / force import.
Moving the assets over
import <conn> <table>(FSAssets' console command) reads a legacy asset table and stores every row. Its implementation (MySQLFSAssetData.Import) opens<conn>as a MySQL connection, so it migrates a MySQLassetstable, notAsset.db.- From
Asset.db: through archives. Save the region withsave oarand each user's inventory withsave iaron the old install (backup.md;tools/backup.pydoes both withWORLDBRIDGE_ALLOW_CONSOLE=true), start the new install with FSAssets, andload oar/load iar. An OAR carries every asset the region's objects and terrain use; an IAR every asset in that inventory. Assets nothing references are left behind, which is the cleanup you want anyway. Check the result in a viewer before retiring the old store. FallbackService(FSAssets fetches a missing asset from another asset service and stores it) is no shortcut here: the fallbackAssetServicereads the same[AssetService]section, so it would look in the MySQL connection too, not inAsset.db.
Keeping the SQLite store small
-
Unreferenced script copies:
tools/prune_script_assets.py. WorldBridge'sset_scriptonce stored a new script asset on every call and never removed the one it replaced; it now reuses unchanged text and deletes a superseded copy it can prove unused (worldbridge/HARDENING.md), but the old copies stay inAsset.db. The tool finds every script asset that no prim item, no user inventory item, no object or notecard asset and no Library entry references, and with--applydeletes exactly those from the file it is given. It works on COPIES taken while the server is stopped and refuses a livebin/(opensim-src/bin,PARACOSM_BIN, a folder holdingOpenSim.dll, or a database with a-journal/-walbeside it):# server stopped python tools/prune_script_assets.py --asset-db C:/prune/Asset.db --opensim-db C:/prune/OpenSim.db --inventory-db C:/prune/inventory.db python tools/prune_script_assets.py ... --apply --vacuumOn this house's copies it found 16,741 unreferenced script rows (18.9 MB of script text);
--vacuumshrank the file by 27.7 MB. The full procedure, and putting the pruned copy back, are inworldbridge/HARDENING.mdsection 2. -
Repeated uploads are the bigger share (above: 491 MB of texture and 219 MB of mesh bytes repeat other rows). The store cannot merge them after the fact: each row's UUID is referenced by the objects that use it. The remedy is at upload time, reusing the existing asset when the bytes are identical, as
set_scriptnow does for script text.upload_textureandspawn_meshstore a new asset on every call;spawn_mesh_instancereuses a mesh asset, but only when the caller passes one. A builder that re-runs can keep the texture and mesh asset ids it got the first time and pass them again instead of uploading. -
The Flotsam cache never expires with the shipped settings. Upstream's
FlotsamCache.ini.examplesetsFileCacheTimeout = 48(hours) butFileCleanupTimer = 0.0(disabled), so files are never removed. SetFileCleanupTimer = 1.0inbin/config-include/FlotsamCache.inito have files older than 48 hours cleaned every hour, or run the console'sfcache expire <mm/dd/yyyy>orfcache clearnow and then.