Skip to content

Commit 3542073

Browse files
authored
Add metadata via config strings (LLNL#595)
* Function to add metadata via ConfigManager * Add test for ConfigManager metadata import * Add short documentation for ConfigManager metadata
1 parent 6790798 commit 3542073

File tree

5 files changed

+207
-19
lines changed

5 files changed

+207
-19
lines changed

doc/sphinx/CaliperBasics.rst

+19
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,25 @@ Adiak (Adiak support must be enabled in the Caliper build configuration). The
613613
spot config for the Spot web visualization framework requires that metadata
614614
attributes are recorded through Adiak.
615615

616+
Finally, metadata can be added through the ``CALI_CONFIG`` and ConfigManager
617+
configuration strings with the ``metadata`` keyword:
618+
619+
.. code-block:: sh
620+
621+
$ CALI_CONFIG="runtime-report,print.metadata,metadata(foo=fooval,bar=barval)" ./examples/apps/cxx-example
622+
caliper.config :
623+
iterations : 4
624+
cali.caliper.version : 2.12.0-dev
625+
opts:print.metadata : true
626+
opts:output.append : true
627+
opts:order_as_visited : true
628+
foo : fooval
629+
bar : barval
630+
cali.channel : runtime-report
631+
Path Time (E) Time (I) Time % (E) Time % (I)
632+
main 0.000014 0.000547 2.118374 85.205938
633+
...
634+
616635
Third-party tool support (NVidia NSight, Intel VTune)
617636
-----------------------------------------------------
618637

src/caliper/ChannelController.cpp

+2-5
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,9 @@ namespace
3939
void add_channel_metadata(Caliper& c, Channel* channel, const info_map_t& metadata)
4040
{
4141
for (const auto &entry : metadata) {
42-
std::string key = channel->name();
43-
key.append(":");
44-
key.append(entry.first);
45-
4642
auto attr =
47-
c.create_attribute(key, CALI_TYPE_STRING, CALI_ATTR_GLOBAL | CALI_ATTR_SKIP_EVENTS);
43+
c.create_attribute(entry.first, CALI_TYPE_STRING,
44+
CALI_ATTR_GLOBAL | CALI_ATTR_SKIP_EVENTS | CALI_ATTR_UNALIGNED);
4845

4946
c.set(channel, attr, Variant(entry.second.c_str()));
5047
}

src/caliper/ConfigManager.cpp

+169-13
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ merge_new_elements(ConfigManager::arglist_t& to, const ConfigManager::arglist_t&
9191
return p.first == v.first;
9292
});
9393

94-
if (it == to.end())
94+
if (it == to.end() || p.first == "metadata") // hacky but we want to allow multiple entries for metadata
9595
to.push_back(p);
9696
}
9797

@@ -180,6 +180,152 @@ expand_variables(const std::string& in, const std::string& val)
180180
return ret;
181181
}
182182

183+
ConfigManager::arglist_t
184+
parse_keyval_list(std::istream& is)
185+
{
186+
ConfigManager::arglist_t ret;
187+
char c = 0;
188+
189+
do {
190+
std::string key = util::read_word(is, "=");
191+
if (key.empty())
192+
return ret;
193+
194+
c = util::read_char(is);
195+
if (c != '=')
196+
return ret;
197+
198+
std::string val = util::read_word(is, ",()");
199+
if (!val.empty())
200+
ret.push_back(std::make_pair(key, val));
201+
202+
c = util::read_char(is);
203+
} while (is.good() && c == ',');
204+
205+
if (is.good())
206+
is.unget();
207+
208+
return ret;
209+
}
210+
211+
std::pair<bool, StringConverter>
212+
find_key_in_json(const std::vector<std::string>& path, const std::map<std::string, StringConverter>& dict)
213+
{
214+
if (path.empty())
215+
return std::make_pair(false, StringConverter());
216+
217+
auto it = dict.find(path.front());
218+
if (it == dict.end())
219+
return std::make_pair(false, StringConverter());
220+
221+
if (path.size() == 1)
222+
return std::make_pair(true, it->second);
223+
224+
StringConverter ret = it->second;
225+
for (auto path_it = path.begin()+1; path_it != path.end(); ++path_it) {
226+
auto sub_dict = ret.rec_dict();
227+
it = sub_dict.find(*path_it);
228+
if (it == sub_dict.end())
229+
return std::make_pair(false, StringConverter());
230+
ret = it->second;
231+
}
232+
233+
return std::make_pair(true, ret);
234+
}
235+
236+
unsigned
237+
add_metadata_entries(const std::string& key, const StringConverter& val, info_map_t& info)
238+
{
239+
unsigned num_entries = 0;
240+
241+
bool is_dict = false;
242+
auto dict = val.rec_dict(&is_dict);
243+
244+
if (is_dict) {
245+
std::string prefix = key + ".";
246+
for (const auto& p : dict) {
247+
info[prefix + p.first] = p.second.to_string();
248+
++num_entries;
249+
}
250+
} else {
251+
info[key] = val.to_string();
252+
++num_entries;
253+
}
254+
255+
return num_entries;
256+
}
257+
258+
void
259+
read_metadata_from_json_file(const std::string& filename, const std::string& keys, info_map_t& info)
260+
{
261+
std::ifstream is(filename.c_str(), std::ios::ate);
262+
263+
if (!is) {
264+
Log(0).stream() << "read_metadata_from_json_file(): Cannot open file "
265+
<< filename << ", quitting\n";
266+
return;
267+
}
268+
269+
auto size = is.tellg();
270+
std::string str(size, '\0');
271+
is.seekg(0);
272+
if (!is.read(&str[0], size)) {
273+
Log(0).stream() << "read_metadata_from_json_file(): Cannot read file "
274+
<< filename << ", quitting\n";
275+
return;
276+
}
277+
278+
bool ok = false;
279+
auto top = StringConverter(str).rec_dict(&ok);
280+
281+
if (!ok) {
282+
Log(0).stream() << "read_metadata_from_json_file(): Cannot parse top-level dict in "
283+
<< filename << ", quitting\n";
284+
return;
285+
}
286+
287+
auto keylist = StringConverter(keys).to_stringlist();
288+
if (keylist.empty()) {
289+
for (const auto& p : top)
290+
keylist.push_back(p.first);
291+
}
292+
293+
for (const std::string& key : keylist) {
294+
std::vector<std::string> path = StringConverter(key).to_stringlist(".");
295+
auto ret = find_key_in_json(path, top);
296+
if (ret.first)
297+
add_metadata_entries(key, ret.second, info);
298+
else
299+
Log(1).stream() << "read_metadata_from_json_file(): Key "
300+
<< key << " not found\n";
301+
}
302+
}
303+
304+
void
305+
add_metadata(const std::string& args, info_map_t& info)
306+
{
307+
std::istringstream is(args);
308+
auto arglist = parse_keyval_list(is);
309+
310+
auto it = std::find_if(arglist.begin(), arglist.end(), [](const std::pair<std::string,std::string>& p){
311+
return p.first == "file";
312+
});
313+
314+
if (it != arglist.end()) {
315+
std::string filename = it->second;
316+
std::string keys;
317+
it = std::find_if(arglist.begin(), arglist.end(), [](const std::pair<std::string,std::string>& p){
318+
return p.first == "keys";
319+
});
320+
if (it != arglist.end())
321+
keys = it->second;
322+
read_metadata_from_json_file(filename, keys, info);
323+
} else {
324+
for (const auto& p : arglist)
325+
info[p.first] = p.second;
326+
}
327+
}
328+
183329
} // namespace [anonymous]
184330

185331

@@ -540,8 +686,15 @@ struct ConfigManager::Options::OptionsImpl
540686

541687
void
542688
update_channel_metadata(info_map_t& info) {
543-
for (const auto &p : args)
544-
info[p.first] = p.second;
689+
for (const auto &p : args) {
690+
if (p.first == "metadata") {
691+
::add_metadata(p.second, info);
692+
} else {
693+
std::string key = "opts:";
694+
key.append(p.first);
695+
info[key] = p.second;
696+
}
697+
}
545698
}
546699

547700
std::string
@@ -614,19 +767,16 @@ struct ConfigManager::Options::OptionsImpl
614767

615768
for (const auto &argp : args) {
616769
auto s_it = spec.data.find(argp.first);
617-
618770
if (s_it == spec.data.end())
619771
continue;
620772

621773
// Non-boolean options are enabled if they are present in args.
622774
// For boolean options, check if they are set to false or true.
623775
bool enabled = true;
624-
625776
if (s_it->second.type == "bool")
626777
enabled = StringConverter(argp.second).to_bool();
627778
if (enabled) {
628779
vec.push_back(argp.first);
629-
630780
auto tmp = get_inherited_specs(argp.first);
631781
vec.insert(vec.end(), tmp.begin(), tmp.end());
632782
}
@@ -745,8 +895,8 @@ struct ConfigManager::ConfigManagerImpl
745895
std::map< std::string, arglist_t >
746896
m_default_parameters_for_spec;
747897

748-
arglist_t m_default_parameters;
749-
argmap_t m_extra_vars;
898+
arglist_t m_default_parameters;
899+
argmap_t m_extra_vars;
750900

751901
OptionSpec m_global_opts;
752902

@@ -893,7 +1043,7 @@ struct ConfigManager::ConfigManagerImpl
8931043
add_config_spec(s.spec, s.create, s.check_args, true /* ignore existing */ );
8941044
}
8951045

896-
// Parse "=value"
1046+
// Parse "=value" or "(value)"
8971047
// Returns an empty string if there is no '=', otherwise the string after '='.
8981048
// Sets error if there is a '=' but no word afterwards.
8991049
std::string
@@ -906,8 +1056,12 @@ struct ConfigManager::ConfigManagerImpl
9061056

9071057
if (val.empty())
9081058
set_error("Expected value after \"" + key + "=\"", is);
909-
}
910-
else
1059+
} else if (c == '(') {
1060+
val = util::read_nested_text(is, '(', ')');
1061+
c = util::read_char(is);
1062+
if (c != ')')
1063+
set_error("Missing ')' after \"" + key + "\"(", is);
1064+
} else
9111065
is.unget();
9121066

9131067
return val;
@@ -931,7 +1085,7 @@ struct ConfigManager::ConfigManagerImpl
9311085
std::string key = util::read_word(is, ",=()\n");
9321086

9331087
if (!key.empty()) {
934-
if (!(opts.contains(key))) {
1088+
if (!(opts.contains(key)) && key != "metadata") {
9351089
set_error("Unknown option: " + key);
9361090
args.clear();
9371091
return args;
@@ -1121,7 +1275,9 @@ struct ConfigManager::ConfigManagerImpl
11211275
if (m_error)
11221276
return ret;
11231277

1124-
if (is_option(key)) {
1278+
if (key == "metadata") {
1279+
m_default_parameters.push_back(std::make_pair(key, val));
1280+
} else if (is_option(key)) {
11251281
if (val.empty())
11261282
val = "true";
11271283

test/ci_app_tests/test_basictrace.py

+16
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,22 @@ def test_globals_selection(self):
151151
self.assertTrue(cat.has_snapshot_with_keys(
152152
snapshots, { 'cali.caliper.version' } ) )
153153

154+
def test_configmanager_metadata_import(self):
155+
target_cmd = [ './ci_test_macros', '0', 'hatchet-region-profile(metadata(bla=garbl),output.format=json-split),metadata(file=example_node_info.json,keys=\"host.os,host.name\"),output=stdout' ]
156+
157+
caliper_config = {
158+
'CALI_LOG_VERBOSITY' : '0'
159+
}
160+
161+
obj = json.loads( cat.run_test(target_cmd, caliper_config)[0] )
162+
163+
self.assertEqual(obj['host.os.name'], 'TestOS')
164+
self.assertEqual(obj['host.os.version'], '3.11')
165+
self.assertEqual(obj['host.name'], 'test42')
166+
self.assertEqual(obj['bla'], 'garbl')
167+
self.assertFalse('other' in obj.keys())
168+
self.assertFalse('host.cluster' in obj.keys())
169+
154170
def test_lcnodeinfo(self):
155171
target_cmd = [ './ci_test_basic' ]
156172
query_cmd = [ '../../src/tools/cali-query/cali-query', '-G', '-e' ]

test/ci_app_tests/test_spot.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def test_spot_regionprofile(self):
5656
self.assertEqual(r['spot.channel'], 'regionprofile')
5757

5858
self.assertEqual('regionprofile', obj['globals']['spot.channels'])
59-
self.assertEqual('true', obj['globals']['spot:region.count'])
59+
self.assertEqual('true', obj['globals']['opts:region.count'])
6060

6161
def test_spot_timeseries(self):
6262
target_cmd = [ './ci_test_macros', '0', 'spot(output=stdout,timeseries,timeseries.iteration_interval=15)', '75' ]

0 commit comments

Comments
 (0)