v2 / vlib / v / vcache / vcache.v
215 lines · 194 sloc · 7.43 KB · 2a66c6aa63251e941ccd415a511821c15fae6bb1
Raw
1module vcache
2
3import os
4import hash
5
6// Using a 2 level cache, ensures a more even distribution of cache entries,
7// so there will not be cramped folders that contain many thousands of them.
8// Most filesystems can not handle performantly such folders, and slow down.
9// The first level will contain a max of 256 folders, named from 00/ to ff/.
10// Each of them will contain many entries, but hopefully < 1000.
11// Note: using a hash here, makes the cache storage immune to special
12// characters in the keys, like quotes, spaces and so on.
13// Cleanup of the cache is simple: just delete the $VCACHE folder.
14// The cache tree will look like this:
15// │ $VCACHE
16// │ ├── README.md <-- a short description of the folder's purpose.
17// │ ├── 0f
18// │ │ ├── 0f004f983ab9c487b0d7c1a0a73840a5.txt
19// │ │ ├── 0f599edf5e16c2756fbcdd4c865087ac.output.description.txt <-- build details
20// │ │ └── 0f599edf5e16c2756fbcdd4c865087ac.vh
21// │ ├── 29
22// │ │ ├── 294717dd02a1cca5f2a0393fca2c5c22.o
23// │ │ └── 294717dd02a1cca5f2a0393fca2c5c22.output.description.txt <-- build details
24// │ ├── 62
25// │ │ └── 620d60d6b81fdcb3cab030a37fd86996.h
26// │ └── 76
27// │ └── 7674f983ab9c487b0d7c1a0ad73840a5.c
28pub struct CacheManager {
29pub:
30 basepath string
31 original_vopts string
32pub mut:
33 vopts string
34 k2cpath map[string]string // key -> filesystem cache path for the object
35}
36
37fn remove_old_cache_folder() {
38 // TODO: remove this after bootstrapping the new .cache location, i.e. after 2024-12-01
39 old_cache_folder := os.join_path(os.vmodules_dir(), 'cache')
40 if os.exists(old_cache_folder) {
41 old_readme_file := os.join_path(old_cache_folder, 'README.md')
42 if os.file_size(old_readme_file) == 254 {
43 os.rmdir_all(old_cache_folder) or {}
44 dlog(@FN, 'old_cache_folder: ${old_cache_folder}')
45 }
46 }
47}
48
49pub fn new_cache_manager(opts []string) CacheManager {
50 // use a path, that would not conflict with a user installable module. `import .cache` is not valid, => better than just `cache`:
51 vcache_basepath := os.getenv_opt('VCACHE') or { os.join_path(os.vmodules_dir(), '.cache') }
52 nlog(@FN,
53 'vcache_basepath: ${vcache_basepath}\n opts: ${opts}\n os.args: ${os.args.join(' ')}')
54 dlog(@FN, 'vcache_basepath: ${vcache_basepath} | opts:\n ${opts}')
55 if !os.is_dir(vcache_basepath) {
56 remove_old_cache_folder()
57 os.mkdir_all(vcache_basepath, mode: 0o700) or { panic(err) } // keep directory private
58 dlog(@FN, 'created folder:\n ${vcache_basepath}')
59 }
60 readme_file := os.join_path(vcache_basepath, 'README.md')
61 if !os.is_file(readme_file) {
62 readme_content := 'This folder contains cached build artifacts from the V build system.
63 |You can safely delete it, if it is getting too large.
64 |It will be recreated the next time you compile something with V.
65 |You can change its location with the VCACHE environment variable.
66 '.strip_margin()
67 os.write_file(readme_file, readme_content) or { panic(err) }
68 dlog(@FN, 'created readme_file:\n ${readme_file}')
69 }
70 mut deduped_opts := map[string]bool{}
71 for o in opts {
72 deduped_opts[o] = true
73 }
74 deduped_opts_keys := deduped_opts.keys().filter(it != '' && !it.starts_with("['gcboehm', "))
75 // TODO: do not filter the gcboehm options here, instead just start `v build-module vlib/builtin` without the -d gcboehm etc.
76 // Note: the current approach of filtering the gcboehm keys may interfere with (potential) other gc modes.
77 original_vopts := deduped_opts_keys.join('|')
78 return CacheManager{
79 basepath: vcache_basepath
80 vopts: original_vopts
81 original_vopts: original_vopts
82 }
83}
84
85// set_temporary_options can be used to add temporary options to the hash salt
86// Note: these can be changed easily with another .set_temporary_options call
87// without affecting the .original_vopts
88pub fn (mut cm CacheManager) set_temporary_options(new_opts []string) {
89 cm.vopts = cm.original_vopts + '#' + new_opts.join('|')
90 cm.k2cpath = map[string]string{}
91 dlog(@FN, 'cm.vopts:\n ${cm.vopts}')
92}
93
94pub fn (mut cm CacheManager) key2cpath(key string) string {
95 mut cpath := cm.k2cpath[key] or { '' }
96 if cpath == '' {
97 hk := cm.vopts + key
98 a := hash.sum64_string(hk, 5).hex_full()
99 b := hash.sum64_string(hk, 7).hex_full()
100 khash := a + b
101 prefix := khash[0..2]
102 cprefix_folder := os.join_path(cm.basepath, prefix)
103 cpath = os.join_path(cprefix_folder, khash)
104 if !os.is_dir(cprefix_folder) {
105 os.mkdir_all(cprefix_folder) or {
106 // The error here may be due to a race with another independent V process, that has already created the same folder.
107 // If that is the case, so be it - just reuse the folder ¯\_(ツ)_/¯ ...
108 if !os.is_dir(cprefix_folder) {
109 panic(err)
110 }
111 }
112 }
113 dlog(@FN, 'new hk')
114 dlog(@FN, ' key: ${key}')
115 dlog(@FN, ' cpath: ${cpath}')
116 dlog(@FN, ' cm.vopts:\n ${cm.vopts}')
117 cm.k2cpath[key] = cpath
118 }
119 dlog(@FN, 'key: ${key:-30} => cpath: ${cpath}')
120 return cpath
121}
122
123pub fn (mut cm CacheManager) postfix_with_key2cpath(postfix string, key string) string {
124 prefix := cm.key2cpath(key)
125 res := prefix + postfix
126 return res
127}
128
129fn normalise_mod(mod string) string {
130 return mod.replace('/', '.').replace('\\', '.').replace('vlib.', '').trim('.')
131}
132
133pub fn (mut cm CacheManager) mod_postfix_with_key2cpath(mod string, postfix string, key string) string {
134 prefix := cm.key2cpath(key)
135 res := '${prefix}.module.${normalise_mod(mod)}${postfix}'
136 return res
137}
138
139pub fn (mut cm CacheManager) exists(postfix string, key string) !string {
140 fpath := cm.postfix_with_key2cpath(postfix, key)
141 dlog(@FN, 'postfix: ${postfix} | key: ${key} | fpath: ${fpath}')
142 if !os.exists(fpath) {
143 return error('does not exist yet')
144 }
145 return fpath
146}
147
148pub fn (mut cm CacheManager) mod_exists(mod string, postfix string, key string) !string {
149 fpath := cm.mod_postfix_with_key2cpath(mod, postfix, key)
150 dlog(@FN, 'mod: ${mod} | postfix: ${postfix} | key: ${key} | fpath: ${fpath}')
151 if !os.exists(fpath) {
152 return error('does not exist yet')
153 }
154 return fpath
155}
156
157//
158
159pub fn (mut cm CacheManager) save(postfix string, key string, content string) !string {
160 fpath := cm.postfix_with_key2cpath(postfix, key)
161 os.write_file(fpath, content)!
162 dlog(@FN, 'postfix: ${postfix} | key: ${key} | fpath: ${fpath}')
163 return fpath
164}
165
166pub fn (mut cm CacheManager) mod_save(mod string, postfix string, key string, content string) !string {
167 fpath := cm.mod_postfix_with_key2cpath(mod, postfix, key)
168 os.write_file(fpath, content)!
169 dlog(@FN, 'mod: ${mod} | postfix: ${postfix} | key: ${key} | fpath: ${fpath}')
170 return fpath
171}
172
173//
174
175pub fn (mut cm CacheManager) load(postfix string, key string) !string {
176 fpath := cm.exists(postfix, key)!
177 content := os.read_file(fpath)!
178 dlog(@FN, 'postfix: ${postfix} | key: ${key} | fpath: ${fpath}')
179 return content
180}
181
182pub fn (mut cm CacheManager) mod_load(mod string, postfix string, key string) !string {
183 fpath := cm.mod_exists(mod, postfix, key)!
184 content := os.read_file(fpath)!
185 dlog(@FN, 'mod: ${mod} | postfix: ${postfix} | key: ${key} | fpath: ${fpath}')
186 return content
187}
188
189@[if trace_usecache ?]
190pub fn dlog(fname string, s string) {
191 xlog(fname, s)
192}
193
194@[if trace_usecache_n ?]
195fn nlog(fname string, s string) {
196 xlog(fname, s)
197}
198
199fn xlog(fname string, s string) {
200 pid := unsafe { mypid() }
201 if fname[0] != `|` {
202 eprintln('> VCache | pid: ${pid} | CacheManager.${fname} ${s}')
203 } else {
204 eprintln('> VCache | pid: ${pid} ${fname} ${s}')
205 }
206}
207
208@[unsafe]
209fn mypid() int {
210 mut static pid := 0
211 if pid == 0 {
212 pid = os.getpid()
213 }
214 return pid
215}
216