persistent_term(3erl)

๐Ÿฆ‹ persistent_term โ€” Erlang Module Definition, but make it group chat ๐Ÿฆ‹

Okay bestie, welcome to the manual page for persistent_term โ€” Erlang Module Definition energy, but make it useful. ๐Ÿ’…

โœจ NAME โœจ

persistent_term โ€” Persistent terms. Thatโ€™s the name, and yes, itโ€™s giving main character storage. ๐ŸŽ€

๐Ÿ’… DESCRIPTION ๐Ÿ’…

Okay so persistent_term is similar to ets in that it gives you storage for Erlang terms that can be accessed in constant time. The difference? It has been highkey optimized for reading terms at the expense of writing and updating terms. Obsessed with reads, lowkey allergic to writes. ๐Ÿ˜ญ

When a persistent term is updated or deleted, a global garbage collection pass is run to scan all processes for the deleted term, and to copy it into each process that still uses it. So persistent_term is suitable for storing Erlang terms that are frequently accessed but never or infrequently updated. Fr fr, read-heavy and write-light is the vibe. ๐Ÿฆ‹

โš ๏ธ Warning: Persistent terms is an advanced feature and is not a general replacement for ETS tables. Before using persistent terms, make sure to fully understand the consequence to system performance when updating or deleting persistent terms. Like, genuinely understand it. No cap. ๐Ÿ’€

Term lookup (using get/1) is done in constant time and without taking any locks, and the term is not copied to the heap (as is the case with terms stored in ETS tables). Literally iconic behavior. โœจ

Storing or updating a term (using put/2) is proportional to the number of already created persistent terms because the hash table holding the keys will be copied. In addition, the term itself will be copied. So yeah, writes are doing cardio. ๐Ÿƒโ€โ™€๏ธ๐Ÿ’จ

When a (complex) term is deleted (using erase/1) or replaced by another (using put/2), a global garbage collection is initiated. It works like this:

  • All processes in the system will be scheduled to run a scan of their heaps for the term that has been deleted. While such scan is relatively light-weight, if there are many processes, the system can become less responsive until all processes have scanned their heaps. ๐Ÿซ 
  • If the deleted term (or any part of it) is still used by a process, that process will do a major (fullsweep) garbage collection and copy the term into the process. However, at most two processes at a time will be scheduled to do that kind of garbage collection. The way thatโ€™s a whole system-wide moment. ๐Ÿ˜ญ

Deletion of atoms and other terms that fit in one machine word is specially optimized to avoid doing a global GC. It is still not recommended to update persistent terms with such values too frequently because the hash table holding the keys is copied every time a persistent term is updated. Cute optimization, still donโ€™t spam it. ๐Ÿ’…

Some suitable uses for persistent terms are:

  • Storing of configuration data that must be easily accessible by all processes. ๐Ÿ› ๏ธ
  • Storing of references for NIF resources. ๐Ÿงš
  • Storing of references for efficient counters. ๐Ÿ”ข
  • Storing an atom to indicate a logging level or whether debugging is turned on. ๐Ÿชต

๐Ÿงš STORING HUGE PERSISTENT TERMS ๐Ÿงš

The current implementation of persistent terms uses the literal allocator also used for literals (constant terms) in BEAM code. By default, 1 GB of virtual address space is reserved for literals in BEAM code and persistent terms. The amount of virtual address space reserved for literals can be changed by using the +MIscs option when starting the emulator. Big-term besties, this oneโ€™s for you. ๐Ÿซถ

Here is an example how the reserved virtual address space for literals can be raised to 2 GB (2048 MB):

erl +MIscs 2048

๐ŸŒธ BEST PRACTICES FOR USING PERSISTENT TERMS ๐ŸŒธ

It is recommended to use keys like ?MODULE or {?MODULE,SubKey} to avoid name collisions. Keep it namespaced, bestie. ๐Ÿท๏ธ

Prefer creating a few large persistent terms to creating many small persistent terms. The execution time for storing a persistent term is proportional to the number of already existing terms. Fewer, bigger, chiller. ๐Ÿง˜โ€โ™€๏ธ

Updating a persistent term with the same value as it already has is specially optimized to do nothing quickly; thus, there is no need to compare the old and new values and avoid calling put/2 if the values are equal. Itโ€™s giving โ€œif it ainโ€™t changed, donโ€™t touch it.โ€ ๐Ÿ’…

When atoms or other terms that fit in one machine word are deleted, no global GC is needed. Therefore, persistent terms that have atoms as their values can be updated more frequently, but note that updating such persistent terms is still much more expensive than reading them. Reads stay winning. ๐Ÿ‘‘

Updating or deleting a persistent term will trigger a global GC if the term does not fit in one machine word. Processes will be scheduled as usual, but all processes will be made runnable at once, which will make the system less responsive until all processes have run and scanned their heaps for the deleted terms. One way to minimize the effects on responsiveness could be to minimize the number of processes on the node before updating or deleting a persistent term. It would also be wise to avoid updating terms when the system is at peak load. Peak load is not the moment, bestie. ๐Ÿšซ๐Ÿ”ฅ

Avoid storing a retrieved persistent term in a process if that persistent term could be deleted or updated in the future. If a process holds a reference to a persistent term when the term is deleted, the process will be garbage collected and the term copied to the process. So donโ€™t cling to the old term like itโ€™s a situationship. ๐Ÿ™ˆ

Avoid updating or deleting more than one persistent term at a time. Each deleted term will trigger its own global GC. That means that deleting N terms will make the system less responsive N times longer than deleting a single persistent term. Therefore, terms that are to be updated at the same time should be collected into a larger term, for example, a map or a tuple. Bundle the glow-up, donโ€™t make it do separate errands. ๐Ÿ›๏ธ

๐ŸŽ€ EXAMPLE ๐ŸŽ€

The following example shows how lock contention for ETS tables can be minimized by having one ETS table for each scheduler. The table identifiers for the ETS tables are stored as a single persistent term:

%% There is one ETS table for each scheduler.
Sid = erlang:system_info(scheduler_id),
Tid = element(Sid, persistent_term:get(?MODULE)),
ets:update_counter(Tid, Key, 1).

Ate. No notes. ๐Ÿ”ฅ

๐Ÿฆ‹ DATA TYPES ๐Ÿฆ‹

key() = term()

Any Erlang term. ๐Ÿซถ

value() = term()

Any Erlang term. ๐Ÿ’–

๐Ÿ‘‘ EXPORTS ๐Ÿ‘‘

erase(Key) -> Result

Types:

Key = key()
Result = boolean()

Erase the name for the persistent term with key Key. The return value will be true if there was a persistent term with the key Key, and false if there was no persistent term associated with the key. Clean, simple, boolean. ๐Ÿ’…

If there existed a previous persistent term associated with key Key, a global GC has been initiated when erase/1 returns. See Description. This is the part where the whole system feels it. ๐Ÿ˜ญ

get() -> List

Types:

List = [{key(), value()}]

Retrieve the keys and values for all persistent terms. The keys will be copied to the heap for the process calling get/0, but the values will not. Selective copying, we love to see it. โœจ

get(Key) -> Value

Types:

Key = key()
Value = value()

Retrieve the value for the persistent term associated with the key Key. The lookup will be made in constant time and the value will not be copied to the heap of the calling process. Constant-time icon behavior. ๐Ÿ‘‘

This function fails with a badarg exception if no term has been stored with the key Key. So if itโ€™s not there, itโ€™s not there โ€” and it will tell you loudly. ๐Ÿ’€

If the calling process holds on to the value of the persistent term and the persistent term is deleted in the future, the term will be copied to the process. Hold on if you must, but know the consequences. ๐Ÿซ 

get(Key, Default) -> Value

Types:

Key = key()
Default = Value = value()

Retrieve the value for the persistent term associated with the key Key. The lookup will be made in constant time and the value will not be copied to the heap of the calling process. Same iconic speed, now with a fallback. ๐ŸŒˆ

This function returns Default if no term has been stored with the key Key. No badarg, just default energy. ๐ŸŽ€

If the calling process holds on to the value of the persistent term and the persistent term is deleted in the future, the term will be copied to the process. Same caveat, same drama. ๐Ÿ’…

info() -> Info

Types:

Info = #{count := Count, memory := Memory}
Count = Memory = integer() >= 0

Return information about persistent terms in a map. The map has the following keys:

  • count: The number of persistent terms. ๐Ÿ”ข
  • memory: The total amount of memory (measured in bytes) used by all persistent terms. ๐Ÿงฎ

put(Key, Value) -> ok

Types:

Key = key()
Value = value()

Store the value Value as a persistent term and associate it with the key Key. Put it there, bestie. ๐Ÿซถ

If the value Value is equal to the value previously stored for the key, put/2 will do nothing and return quickly. Love a no-op slay. โœจ

If there existed a previous persistent term associated with key Key, a global GC has been initiated when put/2 returns. See Description. Again: global GC is the main character of that moment. ๐Ÿ˜ญ


Footer flex: Ericsson AB ยท erts 13.2.2.5 ยท no date shown in the footer, so this is the cute sign-off instead. ๐Ÿซถ๐Ÿ’–

๐Ÿ“ฆ where this page comes from