Skip to content

Commit 57c74c9

Browse files
committed
(GH-236) Add an experimental persistant cache layer
Previously the puppet assets were always loaded from Puppet on every invocation of the Langauge server. This incurs a large startup penalty. This commit adds the ability to save and load cache results to disk. The cache results use the SHA256 of the source files and version information to invalidate cache entries.
1 parent a20a855 commit 57c74c9

File tree

10 files changed

+120
-11
lines changed

10 files changed

+120
-11
lines changed

client/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how
99
- ([GH-225](https://github.com/jpogran/puppet-vscode/issues/225)) Readd Local Workspace comand line option
1010
- ([GH-231](https://github.com/jpogran/puppet-vscode/issues/231)) Make Document Validation asynchronous
1111
- ([GH-236](https://github.com/jpogran/puppet-vscode/issues/236)) Remove the preload option
12+
- ([GH-236](https://github.com/jpogran/puppet-vscode/issues/236)) Add experimental file cache option
1213

1314
## 0.9.0 - 2018-02-01
1415

client/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,11 @@
288288
"default": 10,
289289
"description": "The timeout to connect to the local Puppet Language Server"
290290
},
291+
"puppet.languageserver.filecache.enable": {
292+
"type": "boolean",
293+
"default": false,
294+
"description": "(Experimental) Enable a persistent file cache for Puppet objects in the Language Server"
295+
},
291296
"puppet.languageserver.debugFilePath": {
292297
"type": "string",
293298
"default": "",

client/src/configuration.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,18 @@ export class ConnectionConfiguration implements IConnectionConfiguration {
1010
public host: string = undefined;
1111
public port: number = undefined;
1212
public timeout: number = undefined;
13+
public enableFileCache: boolean = undefined;
1314
public debugFilePath: string = undefined;
1415
public puppetAgentDir: string = undefined;
1516

1617
constructor(context: vscode.ExtensionContext) {
1718
let config = vscode.workspace.getConfiguration('puppet');
1819

19-
this.host = config['languageserver']['address'];
20-
this.port = config['languageserver']['port'];
21-
this.timeout = config['languageserver']['timeout'];
22-
this.debugFilePath = config['languageserver']['debugFilePath'];
20+
this.host = config['languageserver']['address'];
21+
this.port = config['languageserver']['port'];
22+
this.timeout = config['languageserver']['timeout'];
23+
this.enableFileCache = config['languageserver']['filecache']['enable'];
24+
this.debugFilePath = config['languageserver']['debugFilePath'];
2325

2426
this.puppetAgentDir = config['puppetAgentDir'];
2527
}

client/src/connection.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,9 @@ export class ConnectionManager implements IConnectionManager {
170170
}
171171
args.push('--port=' + this.connectionConfiguration.port);
172172
args.push('--timeout=' + this.connectionConfiguration.timeout);
173+
if (this.connectionConfiguration.enableFileCache) {
174+
args.push('--enable-file-cache');
175+
}
173176
if ((this.connectionConfiguration.debugFilePath != undefined) && (this.connectionConfiguration.debugFilePath != '')) {
174177
args.push('--debug=' + this.connectionConfiguration.debugFilePath);
175178
}

client/src/debugAdapter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class DebugConfiguration implements IConnectionConfiguration {
4343
public host: string = "127.0.0.1";
4444
public port: number = 8082;
4545
public timeout: number = 10;
46+
public enableFileCache: boolean = undefined;
4647
public debugFilePath: string; // = "STDOUT";
4748
public puppetAgentDir: string;
4849
}

client/src/interfaces.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export interface IConnectionConfiguration {
2121
host: string;
2222
port: number;
2323
timeout: number;
24+
enableFileCache: boolean;
2425
debugFilePath: string;
2526
puppetAgentDir: string;
2627
}

server/lib/puppet-languageserver.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ def self.parse(options)
7373
args[:stdio] = true
7474
end
7575

76+
opts.on('--enable-file-cache', 'Enables the file system cache for Puppet Objects (types, class etc.)') do |_misc|
77+
args[:cache] = {
78+
:persistent_cache => :file
79+
}
80+
end
81+
7682
opts.on('--local-workspace=PATH', 'The workspace or file path that will be used to provide module-specific functionality. Default is no workspace path.') do |path|
7783
args[:workspace] = path
7884
end

server/lib/puppet-languageserver/puppet_helper.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
require 'puppet/indirector/face'
22
require 'pathname'
33

4-
%w[puppet_helper/faux_objects puppet_helper/cache].each do |lib|
5-
begin
6-
require "puppet-languageserver/#{lib}"
7-
rescue LoadError
8-
require File.expand_path(File.join(File.dirname(__FILE__), lib))
9-
end
4+
%w[puppet_helper/faux_objects puppet_helper/cache puppet_helper/file_cache].each do |lib|
5+
begin
6+
require "puppet-languageserver/#{lib}"
7+
rescue LoadError
8+
require File.expand_path(File.join(File.dirname(__FILE__), lib))
9+
end
1010
end
1111

1212
module PuppetLanguageServer

server/lib/puppet-languageserver/puppet_helper/cache.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,14 @@ def initialize(options = {})
2525
end
2626

2727
def configure_persistent_cache(options = {})
28-
@persistent_cache = nil
28+
options = {} if options.nil?
29+
# Configure the persistent cache if specified
30+
case options[:persistent_cache]
31+
when :file
32+
@persistent_cache = PuppetLanguageServer::PuppetHelper::PersistentFileCache.new(options[:persistent_cache_options])
33+
else
34+
@persistent_cache = nil
35+
end
2936
true
3037
end
3138

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
module PuppetLanguageServer
2+
module PuppetHelper
3+
class PersistentFileCache
4+
attr_reader :cache_dir
5+
6+
def initialize(_inmemory_cache, _options = {})
7+
require 'digest'
8+
require 'json'
9+
10+
@cache_dir = File.join(ENV['TMP'], 'puppet-vscode-cache')
11+
Dir.mkdir(@cache_dir) unless Dir.exist?(@cache_dir)
12+
end
13+
14+
def load(absolute_path, file_key)
15+
cache_file = File.join(cache_dir, cache_filename(file_key))
16+
17+
content = read_cache_file(cache_file)
18+
return nil if content.nil?
19+
20+
json_obj = JSON.parse(content)
21+
22+
# Check that this is from the same language server version
23+
unless json_obj.nil? || json_obj['metadata']['version'] == PuppetVSCode.version
24+
PuppetLanguageServer.log_message(:debug, "[PuppetHelperFileCache::load_from_persistent_cache] Error loading #{absolute_path}: Expected language server version #{PuppetVSCode.version} but found #{json_obj['metadata']['version']}")
25+
json_obj = nil
26+
end
27+
# Check that the source file hash matches
28+
content_hash = calculate_hash(absolute_path) unless json_obj.nil?
29+
if json_obj.nil? || json_obj['metadata']['content_hash'] != content_hash
30+
PuppetLanguageServer.log_message(:debug, "[PuppetHelperFileCache::load_from_persistent_cache] Error loading #{absolute_path}: Expected content_hash of #{content_hash} but found #{json_obj['metadata']['content_hash']}")
31+
json_obj = nil
32+
else
33+
PuppetLanguageServer.log_message(:debug, "[PuppetHelperFileCache::load_from_persistent_cache] Loading #{absolute_path} from cache")
34+
end
35+
36+
json_obj
37+
rescue RuntimeError => detail
38+
PuppetLanguageServer.log_message(:debug, "[PuppetHelperFileCache::load_from_persistent_cache] Error loading #{absolute_path}: #{detail}")
39+
raise
40+
end
41+
42+
def save!(absolute_path, content)
43+
file_key = canonical_path(absolute_path)
44+
cache_file = File.join(cache_dir, cache_filename(file_key))
45+
46+
# Inject metadata
47+
content['metadata']['version'] = PuppetVSCode.version
48+
content['metadata']['content_hash'] = calculate_hash(absolute_path)
49+
50+
save_cache_file(cache_file, content.to_json)
51+
end
52+
53+
private
54+
55+
def save_cache_file(filepath, content)
56+
File.open(filepath, 'wb') { |file| file.write(content) }
57+
true
58+
end
59+
60+
def read_cache_file(filepath)
61+
return nil unless File.exist?(filepath)
62+
63+
File.open(filepath, 'rb') { |file| file.read }
64+
end
65+
66+
def cache_filename(absolute_path)
67+
Digest::SHA256.hexdigest(canonical_path(absolute_path)) + '.txt'
68+
end
69+
70+
def calculate_hash(filepath)
71+
Digest::SHA256.hexdigest(read_cache_file(filepath))
72+
end
73+
74+
def canonical_path(filepath)
75+
# Strictly speaking some file systems are case sensitive but ruby/puppet throws a fit
76+
# with naming if you do
77+
file_key = filepath.downcase
78+
79+
file_key
80+
end
81+
end
82+
end
83+
end

0 commit comments

Comments
 (0)