From 60dd0a09018a4d09239ca54e7a23ed8ff180fd18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Rodr=C3=ADguez?= <36035218+grojeda@users.noreply.github.com> Date: Fri, 16 Oct 2026 14:49:47 +1100 Subject: [PATCH 1/3] feat(oceancurrent): add Gliders asset indexes --- ARGO/oceancurrent/README.md | 89 +++++++++++++++++++ .../oceancurrent_file_server_api.py | 65 ++++++++++++++ .../test_oceancurrent_file_server_api.py | 88 +++++++++++++++++- 3 files changed, 241 insertions(+), 1 deletion(-) diff --git a/ARGO/oceancurrent/README.md b/ARGO/oceancurrent/README.md index 3e9194ff..8dd609cc 100644 --- a/ARGO/oceancurrent/README.md +++ b/ARGO/oceancurrent/README.md @@ -167,3 +167,92 @@ Currently, the script is adaptive to these products: `currentMetersPlot-48` | `\timeseries\currentMetersPlot-48.json` `currentMetersPlot-49` | `\timeseries\currentMetersPlot-49.json` + +## Gliders asset indexes + +The scanner discovers legacy assets under `/mnt/oceancurrent/website/gliders/`. +These proposed product IDs follow the existing `product-subproduct` convention; +registering the JSON files in `ocean-current-api` is a separate future task. +This change does not register products in the API, frontend or Elasticsearch. + +| Proposed product ID | JSON path relative to the website root | Supported assets | +| --- | --- | --- | +| `gliders-mission` | `/gliders/gliders-mission.json` | Standard mission GIFs | +| `gliders-mission-video` | `/gliders/gliders-mission-video.json` | Standard mission MP4s | +| `gliders-navigation` | `/gliders/gliders-navigation.json` | Navigation GIFs | +| `gliders-navigation-video` | `/gliders/gliders-navigation-video.json` | Navigation MP4s, when available | +| `gliders-ts` | `/gliders/gliders-ts.json` | GIFs directly under `missionTS/` | +| `gliders-yearmaps` | `/gliders/gliders-yearmaps.json` | GIFs directly under `yearmaps/` | + +Mission directories must have a name beginning with an ASCII letter, followed +by letters, digits, underscores or hyphens, an eight-digit mission date, an +optional `_nrt`, and `_4d`, `_12d` or `_30d`. Navigation directories use the +same naming convention with a `nav` prefix. Anchored patterns exclude `nav*` +from standard missions, unsupported periods and trailing directory suffixes. +Only files directly inside the matched directories are scanned. Extensions +are case insensitive; HTML, JSON and PHP files are excluded. + +Each mission directory is preserved in `path` and used verbatim as `region`. +This is a mission/version identifier, not a geographic region parsed from the +name. Period and NRT status remain encoded in that identifier. `depth` is +always null. T-S and annual-map records have null `region` and `depth` because +the scanner does not extract metadata from their filenames. For example, +`gliders-mission.json` can contain: + +```json +[ + { + "path": "/gliders/TasEastCoast20260922_nrt_4d", + "productId": "gliders-mission", + "region": "TasEastCoast20260922_nrt_4d", + "depth": null, + "files": [{"name": "2026101500.gif"}] + } +] +``` + +Identical filenames in different missions remain in separate records. Missing +periods or animations are simply absent; no complete set of periods is assumed. +The existing runner writes an index only when matching assets exist. It does +not remove an old index if all matching assets subsequently disappear. + +### Scheduled scanner impact + +After deployment, the existing scheduled scanner automatically processes six +additional configurations, appended after all existing products. No schedule +or scientific generation changes are required. `save_in_product_folder` writes +up to six uniquely named indexes under `gliders/`, replacing those indexes on +subsequent runs. It does not overwrite other product indexes or source assets. + +Each configuration lists the website root and `gliders/`, but only opens +matching asset directories, with a maximum directory depth of two. Mission +folders are listed twice (GIF and MP4); asset contents are never read. +Memory scales with matching paths and the generated records for a configuration. +The existing JSON grouping searches records linearly, so many missions and +frames can also increase generation time. No shared scanner behavior was changed. + +A local temporary-directory experiment used 734 standard and 201 navigation +mission versions, each with 100 GIFs and one MP4, plus 100 T-S and 100 annual +GIFs. Scanning and writing 94,635 assets took 6.23 seconds with Python allocation +tracing enabled, peaked at 75.7 MiB of traced Python allocations, listed 1,884 +directories / 194,713 entries, and wrote 7,032,390 bytes across six indexes. +This is a synthetic workload, not a production estimate: actual frame counts, +filesystem latency and scanner memory headroom are unknown. More frames can +materially increase these costs. Check the existing duration/memory logs and +available scheduling headroom during normal deployment; if insufficient, +defer enabling the new configurations rather than changing shared scanning logic. +No production scan was performed for this change. + +Gliders permission, read or write failures use existing per-configuration error +handling: other configurations continue, but the job ultimately exits nonzero +and may send the existing monitoring notification. Missing Gliders directories +produce no matches and no writes. Resource exhaustion or overlapping scheduled +runs remain possible if the real workload exceeds available headroom; appending +Gliders and limiting traversal reduce interference but do not isolate resources. + +Run the local OceanCurrent tests (all fixtures are copied or built in temporary +directories) with: + +```bash +python3 -m unittest discover -s ARGO/oceancurrent -p 'test_*.py' +``` diff --git a/ARGO/oceancurrent/oceancurrent_file_server_api.py b/ARGO/oceancurrent/oceancurrent_file_server_api.py index b7447817..1dd080fe 100644 --- a/ARGO/oceancurrent/oceancurrent_file_server_api.py +++ b/ARGO/oceancurrent/oceancurrent_file_server_api.py @@ -546,6 +546,71 @@ def _load_api_endpoint(): "filetype": ".gif", "max_layer": 2, "save_in_product_folder": True + }, + # Keep Gliders last so existing products are processed first. + { + "productId": "gliders-mission", + "include": [ + {"path": "^gliders$", "layer": 1}, + {"path": "^(?!nav)[A-Za-z][A-Za-z0-9_-]*[0-9]{8}(?:_nrt)?_(?:4|12|30)d$", "layer": 2} + ], + "filetype": ".gif", + "region_layer": 2, + "max_layer": 2, + "save_in_product_folder": True + }, + { + "productId": "gliders-mission-video", + "include": [ + {"path": "^gliders$", "layer": 1}, + {"path": "^(?!nav)[A-Za-z][A-Za-z0-9_-]*[0-9]{8}(?:_nrt)?_(?:4|12|30)d$", "layer": 2} + ], + "filetype": ".mp4", + "region_layer": 2, + "max_layer": 2, + "save_in_product_folder": True + }, + { + "productId": "gliders-navigation", + "include": [ + {"path": "^gliders$", "layer": 1}, + {"path": "^nav[A-Za-z][A-Za-z0-9_-]*[0-9]{8}(?:_nrt)?_(?:4|12|30)d$", "layer": 2} + ], + "filetype": ".gif", + "region_layer": 2, + "max_layer": 2, + "save_in_product_folder": True + }, + { + "productId": "gliders-navigation-video", + "include": [ + {"path": "^gliders$", "layer": 1}, + {"path": "^nav[A-Za-z][A-Za-z0-9_-]*[0-9]{8}(?:_nrt)?_(?:4|12|30)d$", "layer": 2} + ], + "filetype": ".mp4", + "region_layer": 2, + "max_layer": 2, + "save_in_product_folder": True + }, + { + "productId": "gliders-ts", + "include": [ + {"path": "^gliders$", "layer": 1}, + {"path": "^missionTS$", "layer": 2} + ], + "filetype": ".gif", + "max_layer": 2, + "save_in_product_folder": True + }, + { + "productId": "gliders-yearmaps", + "include": [ + {"path": "^gliders$", "layer": 1}, + {"path": "^yearmaps$", "layer": 2} + ], + "filetype": ".gif", + "max_layer": 2, + "save_in_product_folder": True } ] diff --git a/ARGO/oceancurrent/test_oceancurrent_file_server_api.py b/ARGO/oceancurrent/test_oceancurrent_file_server_api.py index e669047d..ddeba2dc 100644 --- a/ARGO/oceancurrent/test_oceancurrent_file_server_api.py +++ b/ARGO/oceancurrent/test_oceancurrent_file_server_api.py @@ -6,7 +6,11 @@ import unittest.mock from unittest.mock import patch, MagicMock, mock_open -from oceancurrent_file_server_api import main +from pathlib import Path + +from oceancurrent_file_server_api import ( + main, FILE_PATH_CONFIG, scan_files_from_config, save_result_as_json, +) class TestFileServerAPI(unittest.TestCase): @@ -587,6 +591,88 @@ def test_file_structure_explorer(self): self.assertFalse(os.path.exists(not_existed_path)) + +class TestGlidersConfiguration(unittest.TestCase): + """Build representative assets only inside a temporary website root.""" + + def setUp(self): + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.root = Path(temporary.name) + self.configs = [c for c in FILE_PATH_CONFIG + if c["productId"].startswith("gliders-")] + self.expected = {c["productId"]: [] for c in self.configs} + for navigation in (False, True): + category = "navigation" if navigation else "mission" + for mission, periods in (("TasEastCoast20260922_nrt", (4, 12, 30)), + ("TasEastCoast20260811", (4,))): + for period in periods: + directory = ("nav" if navigation else "") + f"{mission}_{period}d" + for extension, suffix in (("gif", ""), ("mp4", "-video")): + # Older missions deliberately have no animation. + if extension == "mp4" and "nrt" not in mission: + continue + name = "2026101500.gif" if extension == "gif" else f"{directory}.mp4" + self.add_asset(directory, name) + self.expected[f"gliders-{category}{suffix}"].append({ + "path": f"/gliders/{directory}", + "productId": f"gliders-{category}{suffix}", + "region": directory, "depth": None, + "files": [{"name": name}], + }) + for name in ("index.html", "data.json", "index.php", "fake.gif.html"): + self.add_asset(directory, name) + for category, directory, name in ( + ("ts", "missionTS", "sl_TasEastCoast20260922_nrt.gif"), + ("yearmaps", "yearmaps", "2026_sl_temp_nrt.gif")): + self.add_asset(directory, name) + self.add_asset(directory, "index.php") + self.add_asset(directory, "ignored.mp4") + self.expected[f"gliders-{category}"].append({ + "path": f"/gliders/{directory}", "productId": f"gliders-{category}", + "region": None, "depth": None, "files": [{"name": name}], + }) + for directory in ("TasEastCoast20260922_nrt_4d_backup", + "TasEastCoast20260922_nrt_40d", "unrelated", + "prefixnavTasEastCoast20260922_nrt_4d_extra"): + self.add_asset(directory, "2026101500.gif") + self.add_asset("TasEastCoast20260922_nrt_4d/nested", "ignored.gif") + sibling = self.root / "gliders_backup" / "TasEastCoast20260922_nrt_4d" + sibling.mkdir(parents=True) + (sibling / "ignored.gif").touch() + + def add_asset(self, directory, name): + target = self.root / "gliders" / directory / name + target.parent.mkdir(parents=True, exist_ok=True) + target.touch() + + def test_exact_indexes_and_repeat_scan(self): + self.assertEqual(len(self.configs), 6) + sentinel = self.root / "gliders" / "unrelated.json" + sentinel.write_text("preserve me") + for _ in range(2): # Generated indexes must not feed back into the next scan. + for config in self.configs: + with self.subTest(product=config["productId"]): + files = scan_files_from_config(self.root, config) + save_result_as_json(files, config, self.root) + output = self.root / "gliders" / f'{config["productId"]}.json' + actual = json.loads(output.read_text()) + self.assertEqual(sorted(actual, key=lambda r: r["path"]), + sorted(self.expected[config["productId"]], + key=lambda r: r["path"])) + self.assertEqual(sentinel.read_text(), "preserve me") + self.assertEqual(set(self.root.rglob("*.json")), + {self.root / "gliders" / f'{c["productId"]}.json' + for c in self.configs} + | {sentinel} + | set(self.root.glob("gliders/*/data.json"))) + + def test_absent_gliders(self): + with tempfile.TemporaryDirectory() as empty: + for config in self.configs: + self.assertEqual(scan_files_from_config(Path(empty), config), []) + + class TestMonitoringFunctions(unittest.TestCase): """Tests for EC2 monitoring and fatal log notification functions.""" From 5d775cebb1feaea4cfec9001c8d08fc81cbe4db3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Rodr=C3=ADguez?= <36035218+grojeda@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:21:37 +1100 Subject: [PATCH 2/3] fix(oceancurrent): support historical Gliders mission names --- ARGO/oceancurrent/README.md | 127 +++++++++++++++--- ARGO/oceancurrent/benchmark_gliders.py | 108 +++++++++++++++ .../fixtures/gliders_historical_missions.txt | 42 ++++++ .../oceancurrent_file_server_api.py | 36 ++++- .../test_oceancurrent_file_server_api.py | 58 ++++++++ 5 files changed, 349 insertions(+), 22 deletions(-) create mode 100644 ARGO/oceancurrent/benchmark_gliders.py create mode 100644 ARGO/oceancurrent/fixtures/gliders_historical_missions.txt diff --git a/ARGO/oceancurrent/README.md b/ARGO/oceancurrent/README.md index 8dd609cc..ba85c7d8 100644 --- a/ARGO/oceancurrent/README.md +++ b/ARGO/oceancurrent/README.md @@ -184,13 +184,42 @@ This change does not register products in the API, frontend or Elasticsearch. | `gliders-ts` | `/gliders/gliders-ts.json` | GIFs directly under `missionTS/` | | `gliders-yearmaps` | `/gliders/gliders-yearmaps.json` | GIFs directly under `yearmaps/` | -Mission directories must have a name beginning with an ASCII letter, followed -by letters, digits, underscores or hyphens, an eight-digit mission date, an -optional `_nrt`, and `_4d`, `_12d` or `_30d`. Navigation directories use the -same naming convention with a `nav` prefix. Anchored patterns exclude `nav*` -from standard missions, unsupported periods and trailing directory suffixes. -Only files directly inside the matched directories are scanned. Extensions -are case insensitive; HTML, JSON and PHP files are excluded. +Mission directories begin with an ASCII letter, followed by letters, digits, +underscores or hyphens and an eight-digit mission date. The date may have a +single-letter mission suffix (for example `Brisbane20180306A` or +`JervisBay20180503a`) or an observed numeric leg `_1` / `_2` (for example +`Perth20110626_1`). The visualisation period is `_4d`, `_12d` or `_30d`. +An optional `_nrt` may occur before **or** after the period, but not both: + +- `TasEastCoast20260922_nrt_4d` +- `Brisbane20180306A_4d` +- `Perth20110626_1_4d` +- `Cooktown20160217_4d_nrt` + +Navigation directories use exactly the same validation with a `nav` prefix. +Anchored patterns exclude `nav*` from standard missions, unsupported periods +and unrecognised directory suffixes. Only files directly inside the matched +directories are scanned. Extensions are case insensitive; HTML, JSON and PHP +files are excluded. + +A read-only inventory on 16 October 2026 contained 2,900 directories. The +updated rules match 2,133 standard and 603 navigation directories, adding 52 +and 18 respectively. All 42 previously excluded directories linked by the +legacy catalogue now match (all 1,362 distinct linked mission directories are +covered). Those 42 names are retained as a regression fixture in +`fixtures/gliders_historical_missions.txt`. The inventory was checked locally; +no production scanner or production filesystem writes were used. + +In addition to the supported `missionTS/` and `yearmaps/`, 162 directories remain +excluded deliberately: + +| Directory category | Count | Reason | +| --- | ---: | --- | +| `_tsfoa` | 126 | Separate scientific variant, outside the canonical products | +| `_vtsfc` | 14 | Separate scientific variant | +| `_vtTsS` / `_vtsTS` | 10 | Separate scientific variants | +| `_10d` | 3 | Outside the requested 4/12/30-day periods | +| `missionTS_*` / `yearmaps_*` | 9 | Development, alternate or junk folders; only exact canonical folders are enabled | Each mission directory is preserved in `path` and used verbatim as `region`. This is a mission/version identifier, not a geographic region parsed from the @@ -231,17 +260,79 @@ Memory scales with matching paths and the generated records for a configuration. The existing JSON grouping searches records linearly, so many missions and frames can also increase generation time. No shared scanner behavior was changed. -A local temporary-directory experiment used 734 standard and 201 navigation -mission versions, each with 100 GIFs and one MP4, plus 100 T-S and 100 annual -GIFs. Scanning and writing 94,635 assets took 6.23 seconds with Python allocation -tracing enabled, peaked at 75.7 MiB of traced Python allocations, listed 1,884 -directories / 194,713 entries, and wrote 7,032,390 bytes across six indexes. -This is a synthetic workload, not a production estimate: actual frame counts, -filesystem latency and scanner memory headroom are unknown. More frames can -materially increase these costs. Check the existing duration/memory logs and -available scheduling headroom during normal deployment; if insufficient, -defer enabling the new configurations rather than changing shared scanning logic. -No production scan was performed for this change. +The opt-in local benchmark is reproducible with: + +```bash +TMPDIR=/var/tmp python3 ARGO/oceancurrent/benchmark_gliders.py +``` + +Use a local temporary filesystem with capacity for about 2.4 million directory +entries. Each scenario uses a fresh subprocess and runs all 48 configurations +through `main()`, with existing development fixtures plus synthetic Gliders. +The no-Gliders baseline runs the same configurations with development fixtures +only. Fixture creation and cleanup are excluded from timing and worker peak RSS. +Monitoring/metadata requests are disabled. No production scanner is invoked. + +Both Gliders scenarios contain 2,133 standard and 603 navigation directories, +including historical naming forms and incomplete periods. Each GIF has a paired +ignored HTML file; every directory also has ignored PHP/JSON files. Twenty +percent of directories lack MP4s. There are 760 T-S and 210 annual GIFs. +The deterministic frame distributions repeat every 100 directories: + +- Representative: 65% have 24 GIFs, 25% have 100, 8% have 352, and 2% have 913. +- Higher load: 25% have 100 GIFs, 50% have 352, and 25% have 913. + +These are illustrative distributions using observed counts, not a census of +production frames. The higher-load case is a stress scenario, not an absolute +upper bound. Synthetic files are empty hard links; only names and metadata are +used by the scanner. Measurements on local disk with warm filesystem caches +and no CPU throttle, on 16 October 2026: + +| Measurement | No Gliders baseline | Representative | Higher load | +| --- | ---: | ---: | ---: | +| Total scanner wall time, all 48 configurations | 0.009 s | 11.66 s | 58.91 s | +| Process CPU time | 0.009 s | 11.66 s | 58.86 s | +| Gliders scanning | 0.0005 s | 3.67 s | 17.52 s | +| Gliders JSON generation | 0 s | 7.92 s | 41.13 s | +| Peak process RSS | 31.3 MiB | 262.1 MiB | 1,167.9 MiB | +| Matched Gliders assets | 0 | 238,976 | 1,167,749 | +| Generated Gliders JSON size | 0 | 16.97 MiB | 78.97 MiB | + +| Index | Representative assets | Higher-load assets | Representative JSON | Higher-load JSON | +| --- | ---: | ---: | ---: | ---: | +| `gliders-mission` | 183,534 | 906,741 | 13,284,209 bytes | 63,908,699 bytes | +| `gliders-mission-video` | 1,706 | 1,706 | 504,447 bytes | 504,447 bytes | +| `gliders-navigation` | 52,284 | 257,850 | 3,788,006 bytes | 18,177,626 bytes | +| `gliders-navigation-video` | 482 | 482 | 147,354 bytes | 147,354 bytes | +| `gliders-ts` | 760 | 760 | 52,493 bytes | 52,493 bytes | +| `gliders-yearmaps` | 210 | 210 | 14,548 bytes | 14,548 bytes | + +The observed existing production baseline was 657 seconds and approximately +1,135 MiB peak RSS, processing about 5.8 million assets. Live cron runs every +two hours with a 50% CPU limit; about 6.7 GB RAM was available at inspection. +The local total times above include only development fixtures for existing +products, **not** the full production workload. As an illustrative allowance, +doubling the added local wall time for the CPU limit gives about 23 seconds +representative / 118 seconds higher load. Adding those to the production +baseline gives approximately 680 / 775 seconds (11.3 / 12.9 minutes), increases +of 3.5% / 17.9%, within the two-hour interval. This scaling is not a measured +production prediction: hardware, cache state, hard-linked metadata and concurrent +filesystem I/O differ. Even budgeting both the old and higher-load memory peaks +additively is roughly 2.3 GiB, below the previously observed available RAM. + +JSON generation accounts for about 68–70% of added runtime. Grouping currently +searches earlier records for each file, costing O(files × directory records). +This is measurable overhead but does not justify a shared scanner change for +these measured workloads and scheduling headroom. If real growth makes it a +bottleneck, the smallest separate optimisation is a dictionary keyed by +`(region, path)` while retaining the existing output list, record order and JSON +schema. No shared scanning or grouping functions were changed here. + +The measured scenarios support deployment without a resource blocker. Check +duration and memory logs after the first normal scheduled run; production I/O +cost and future growth are still uncertainties. Merging activates the new +configurations on the next run because cron downloads the script from `master`. +No cron, deployment or scientific pipeline changes are included. Gliders permission, read or write failures use existing per-configuration error handling: other configurations continue, but the job ultimately exits nonzero diff --git a/ARGO/oceancurrent/benchmark_gliders.py b/ARGO/oceancurrent/benchmark_gliders.py new file mode 100644 index 00000000..553de3ab --- /dev/null +++ b/ARGO/oceancurrent/benchmark_gliders.py @@ -0,0 +1,108 @@ +"""Opt-in local benchmark; creates synthetic assets only in a temporary directory. + +Run: TMPDIR=/var/tmp python3 ARGO/oceancurrent/benchmark_gliders.py +Allow space for approximately 2.4 million directory entries in the stress fixture. +Fixture creation/cleanup are excluded from worker timing and peak RSS. +""" + +import json +import logging +import os +from pathlib import Path +import resource +import shutil +import subprocess +import sys +import tempfile +import time + + +def build_fixture(root, scenario): + source = Path(__file__).parent / "tests/mnt/oceancurrent/website" + shutil.copytree(source, root, dirs_exist_ok=True) + if scenario == "baseline": + return + # Deterministic, illustrative distributions, not a production census. + distribution = ([24] * 65 + [100] * 25 + [352] * 8 + [913] * 2 + if scenario == "representative" + else [100] * 25 + [352] * 50 + [913] * 25) + for prefix, count in (("", 2133), ("nav", 603)): + for index in range(count): + period = (4, 12, 30)[index % 3] + # Includes suffixes, alternate NRT placement, missing periods/videos. + mission = f"{prefix}Mission{index}20260922" + ending = (f"A_nrt_{period}d", f"_1_{period}d", + f"_{period}d_nrt", f"_{period}d")[index % 4] + directory = root / "gliders" / (mission + ending) + directory.mkdir(parents=True) + seed = directory / "2026000000.gif" + seed.touch() + for frame in range(distribution[index % 100]): + # Hard links avoid millions of inodes; every name is a real file. + if frame: + os.link(seed, directory / f"{2026000000 + frame}.gif") + os.link(seed, directory / f"{2026000000 + frame}.html") + if index % 5: + os.link(seed, directory / f"{directory.name}.mp4") + for name in ("index.php", "data.json"): + os.link(seed, directory / name) + for folder, count in (("missionTS", 760), ("yearmaps", 210)): + directory = root / "gliders" / folder + directory.mkdir() + for index in range(count): + (directory / f"figure{index}.gif").touch() + + +def measure(root): + import oceancurrent_file_server_api as scanner + + logging.disable(logging.CRITICAL) + scanner.OCEAN_CURRENT_FILE_ROOT_PATH = str(root) + scanner.OC_API_ENDPOINT = None # No metadata or monitoring requests. + metrics = {"gliders_scan_seconds": 0, "gliders_json_seconds": 0, + "matched_files": {}, "json_bytes": {}} + scan, save = scanner.scan_files_from_config, scanner.save_result_as_json + + def timed_scan(parent_directory, config): + started = time.perf_counter() + files = scan(parent_directory, config) + if config["productId"].startswith("gliders-"): + metrics["gliders_scan_seconds"] += time.perf_counter() - started + metrics["matched_files"][config["productId"]] = len(files) + return files + + def timed_save(files, config, parent_directory): + started = time.perf_counter() + save(files, config, parent_directory) + if config["productId"].startswith("gliders-"): + metrics["gliders_json_seconds"] += time.perf_counter() - started + output = parent_directory / "gliders" / f'{config["productId"]}.json' + metrics["json_bytes"][config["productId"]] = output.stat().st_size + + scanner.scan_files_from_config = timed_scan + scanner.save_result_as_json = timed_save + started, cpu_started = time.perf_counter(), time.process_time() + scanner.main() # All 48 configurations, including existing development fixtures. + metrics["total_seconds"] = time.perf_counter() - started + metrics["cpu_seconds"] = time.process_time() - cpu_started + rss = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss + metrics["peak_rss_mib"] = rss / (1024 ** 2 if sys.platform == "darwin" else 1024) + print(json.dumps(metrics)) + + +if __name__ == "__main__": + if len(sys.argv) == 3 and sys.argv[1] == "--worker": + measure(Path(sys.argv[2])) + else: + for scenario in ("baseline", "representative", "higher-load"): + print(f"Preparing {scenario} temporary fixture...", flush=True) + with tempfile.TemporaryDirectory(prefix="gliders-benchmark-") as temporary: + root = Path(temporary) / "website" + build_fixture(root, scenario) + result = subprocess.run( + [sys.executable, str(Path(__file__).resolve()), "--worker", str(root)], + check=True, capture_output=True, text=True, + ) + # The scanner can print a logging fallback warning before JSON. + metrics = json.loads(result.stdout.splitlines()[-1]) + print(json.dumps({"scenario": scenario, **metrics}), flush=True) diff --git a/ARGO/oceancurrent/fixtures/gliders_historical_missions.txt b/ARGO/oceancurrent/fixtures/gliders_historical_missions.txt new file mode 100644 index 00000000..1d6e8a34 --- /dev/null +++ b/ARGO/oceancurrent/fixtures/gliders_historical_missions.txt @@ -0,0 +1,42 @@ +Brisbane20180306A_12d +Brisbane20180306A_30d +Brisbane20180306A_4d +Brisbane20180306A_nrt_12d +Brisbane20180306A_nrt_30d +Brisbane20180306A_nrt_4d +Brisbane20180306D_12d +Brisbane20180306D_30d +Brisbane20180306D_4d +Brisbane20180306D_nrt_12d +Brisbane20180306D_nrt_30d +Brisbane20180306D_nrt_4d +Brisbane20180320A_12d +Brisbane20180320A_30d +Brisbane20180320A_4d +Brisbane20180320A_nrt_12d +Brisbane20180320A_nrt_30d +Brisbane20180320A_nrt_4d +Brisbane20180320B_12d +Brisbane20180320B_30d +Brisbane20180320B_4d +Brisbane20180320B_nrt_12d +Brisbane20180320B_nrt_30d +Brisbane20180320B_nrt_4d +JervisBay20180503a_12d +JervisBay20180503a_30d +JervisBay20180503a_4d +Perth20110626_1_12d +Perth20110626_1_30d +Perth20110626_1_4d +Perth20110626_2_12d +Perth20110626_2_30d +Perth20110626_2_4d +TimorSea20181017a_12d +TimorSea20181017a_30d +TimorSea20181017a_4d +TimorSea20181017b_12d +TimorSea20181017b_30d +TimorSea20181017b_4d +TwoRocks20180503a_12d +TwoRocks20180503a_30d +TwoRocks20180503a_4d diff --git a/ARGO/oceancurrent/oceancurrent_file_server_api.py b/ARGO/oceancurrent/oceancurrent_file_server_api.py index 1dd080fe..4f10d8e8 100644 --- a/ARGO/oceancurrent/oceancurrent_file_server_api.py +++ b/ARGO/oceancurrent/oceancurrent_file_server_api.py @@ -552,7 +552,14 @@ def _load_api_endpoint(): "productId": "gliders-mission", "include": [ {"path": "^gliders$", "layer": 1}, - {"path": "^(?!nav)[A-Za-z][A-Za-z0-9_-]*[0-9]{8}(?:_nrt)?_(?:4|12|30)d$", "layer": 2} + { + "path": ( + "^(?!nav)[A-Za-z][A-Za-z0-9_-]*[0-9]{8}" + "(?:[A-Za-z]|_[12])?" + "(?:_nrt_(?:4|12|30)d|_(?:4|12|30)d(?:_nrt)?)$" + ), + "layer": 2 + } ], "filetype": ".gif", "region_layer": 2, @@ -563,7 +570,14 @@ def _load_api_endpoint(): "productId": "gliders-mission-video", "include": [ {"path": "^gliders$", "layer": 1}, - {"path": "^(?!nav)[A-Za-z][A-Za-z0-9_-]*[0-9]{8}(?:_nrt)?_(?:4|12|30)d$", "layer": 2} + { + "path": ( + "^(?!nav)[A-Za-z][A-Za-z0-9_-]*[0-9]{8}" + "(?:[A-Za-z]|_[12])?" + "(?:_nrt_(?:4|12|30)d|_(?:4|12|30)d(?:_nrt)?)$" + ), + "layer": 2 + } ], "filetype": ".mp4", "region_layer": 2, @@ -574,7 +588,14 @@ def _load_api_endpoint(): "productId": "gliders-navigation", "include": [ {"path": "^gliders$", "layer": 1}, - {"path": "^nav[A-Za-z][A-Za-z0-9_-]*[0-9]{8}(?:_nrt)?_(?:4|12|30)d$", "layer": 2} + { + "path": ( + "^nav[A-Za-z][A-Za-z0-9_-]*[0-9]{8}" + "(?:[A-Za-z]|_[12])?" + "(?:_nrt_(?:4|12|30)d|_(?:4|12|30)d(?:_nrt)?)$" + ), + "layer": 2 + } ], "filetype": ".gif", "region_layer": 2, @@ -585,7 +606,14 @@ def _load_api_endpoint(): "productId": "gliders-navigation-video", "include": [ {"path": "^gliders$", "layer": 1}, - {"path": "^nav[A-Za-z][A-Za-z0-9_-]*[0-9]{8}(?:_nrt)?_(?:4|12|30)d$", "layer": 2} + { + "path": ( + "^nav[A-Za-z][A-Za-z0-9_-]*[0-9]{8}" + "(?:[A-Za-z]|_[12])?" + "(?:_nrt_(?:4|12|30)d|_(?:4|12|30)d(?:_nrt)?)$" + ), + "layer": 2 + } ], "filetype": ".mp4", "region_layer": 2, diff --git a/ARGO/oceancurrent/test_oceancurrent_file_server_api.py b/ARGO/oceancurrent/test_oceancurrent_file_server_api.py index ddeba2dc..8e81f0e6 100644 --- a/ARGO/oceancurrent/test_oceancurrent_file_server_api.py +++ b/ARGO/oceancurrent/test_oceancurrent_file_server_api.py @@ -667,6 +667,64 @@ def test_exact_indexes_and_repeat_scan(self): | {sentinel} | set(self.root.glob("gliders/*/data.json"))) + def test_historical_missions(self): + # All 42 directories linked by the legacy catalogue and missed originally. + catalogue = Path(__file__).parent / "fixtures/gliders_historical_missions.txt" + names = catalogue.read_text().splitlines() + self.assertEqual(len(names), 42) + # Other confirmed production names: NRT after the period, and suffix + NRT. + names += ["Cooktown20160217_4d_nrt", "Cooktown20160217_12d_nrt", + "Portland20160128_4d_nrt", "JervisBay20180503a_nrt_12d"] + for navigation in (False, True): + category = "navigation" if navigation else "mission" + for name in names: + directory = ("nav" if navigation else "") + name + self.add_asset(directory, "2026101500.gif") + self.add_asset(directory, f"{directory}.mp4") + for suffix, extension in (("", ".gif"), ("-video", ".mp4")): + product = f"gliders-{category}{suffix}" + config = next(c for c in self.configs if c["productId"] == product) + save_result_as_json(scan_files_from_config(self.root, config), + config, self.root) + actual = json.loads((self.root / "gliders" / f"{product}.json").read_text()) + expected = self.expected[product] + [{ + "path": f"/gliders/{('nav' if navigation else '') + name}", + "productId": product, + "region": ("nav" if navigation else "") + name, + "depth": None, + "files": [{"name": "2026101500.gif" if extension == ".gif" + else f"{('nav' if navigation else '') + name}.mp4"}], + } for name in names] + self.assertEqual(sorted(actual, key=lambda r: r["path"]), + sorted(expected, key=lambda r: r["path"])) + + def test_exclude_unconfirmed_variants(self): + names = ["Brisbane20180306AB_4d", "Perth20110626_3_4d", + "Cooktown20160217_nrt_4d_nrt", "Cooktown20160217_4d_NRT", + "Mission20260922_4d_backup", "Mission20260922_4d_tsfoa", + "Mission20260922_12d_vtsfc", "Mission20260922_nrt_4d_vtTsS", + "Mission20260922_nrt_4d_vtsTS", "Mission20260922_10d", + "missionTS_sl_dev1", "yearmaps_junk"] + for prefix in ("", "nav"): + for name in names: + for extension in ("gif", "mp4"): + self.add_asset(prefix + name, "unrelated." + extension) + for config in self.configs: + files = scan_files_from_config(self.root, config) + self.assertFalse(any(f.stem == "unrelated" for f in files)) + + def test_optional_products_absent(self): + for asset in (self.root / "gliders").rglob("*.mp4"): + asset.unlink() + shutil.rmtree(self.root / "gliders/missionTS") + shutil.rmtree(self.root / "gliders/yearmaps") + for config in self.configs: + files = scan_files_from_config(self.root, config) + if config["productId"] in ("gliders-mission", "gliders-navigation"): + self.assertEqual(len(files), 4) # Three NRT periods + one older period. + else: + self.assertEqual(files, []) + def test_absent_gliders(self): with tempfile.TemporaryDirectory() as empty: for config in self.configs: From c8a7446f44dd6ddfe49ab4c57d29865938cbd390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Rodr=C3=ADguez?= <36035218+grojeda@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:42:50 +1100 Subject: [PATCH 3/3] test(oceancurrent): simplify Gliders fixtures and documentation --- ARGO/oceancurrent/README.md | 190 ++---------- ARGO/oceancurrent/benchmark_gliders.py | 108 ------- .../fixtures/gliders_historical_missions.txt | 42 --- .../test_oceancurrent_file_server_api.py | 292 +++++++++--------- .../gliders/Brisbane20180306AB_4d/ignored.gif | 0 .../Brisbane20180306A_4d/2018031500.gif | 0 .../Brisbane20180306A_4d_tsfoa/ignored.gif | 0 .../Cooktown20160217_4d_nrt/2016022000.gif | 0 .../Cooktown20160217_nrt_4d_nrt/ignored.gif | 0 .../JervisBay20180503a_nrt_12d/2018051000.gif | 0 .../gliders/Perth20110626_1_4d/2011070100.gif | 0 .../gliders/Perth20110626_3_4d/ignored.gif | 0 .../TasEastCoast20260811_4d/2026101500.gif | 0 .../2026101500.gif | 0 .../2026101500.gif | 0 .../TasEastCoast20260922_nrt_40d/ignored.gif | 0 .../2026101500.gif | 0 .../2026101512.gif | 0 .../TasEastCoast20260922_nrt_4d.mp4 | 0 .../TasEastCoast20260922_nrt_4d/data.JSON | 0 .../TasEastCoast20260922_nrt_4d/fake.gif.html | 0 .../TasEastCoast20260922_nrt_4d/index.html | 0 .../TasEastCoast20260922_nrt_4d/index.php | 0 .../nested/ignored.gif | 0 .../ignored.gif | 0 .../website/gliders/missionTS/ignored.mp4 | 0 .../missionTS/sl_TasEastCoast20260922_nrt.gif | 0 .../navBrisbane20180306AB_4d/ignored.gif | 0 .../2018031500.gif | 0 .../navCooktown20160217_4d_nrt/2016022000.gif | 0 .../2018051000.gif | 0 .../navPerth20110626_1_4d/2011070100.gif | 0 .../2026101500.gif | 0 .../navTasEastCoast20260922_nrt_4d.mp4 | 0 .../gliders/yearmaps/2026_sl_temp_nrt.gif | 0 .../website/gliders/yearmaps_junk/ignored.gif | 0 36 files changed, 177 insertions(+), 455 deletions(-) delete mode 100644 ARGO/oceancurrent/benchmark_gliders.py delete mode 100644 ARGO/oceancurrent/fixtures/gliders_historical_missions.txt create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/Brisbane20180306AB_4d/ignored.gif create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/Brisbane20180306A_4d/2018031500.gif create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/Brisbane20180306A_4d_tsfoa/ignored.gif create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/Cooktown20160217_4d_nrt/2016022000.gif create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/Cooktown20160217_nrt_4d_nrt/ignored.gif create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/JervisBay20180503a_nrt_12d/2018051000.gif create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/Perth20110626_1_4d/2011070100.gif create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/Perth20110626_3_4d/ignored.gif create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260811_4d/2026101500.gif create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_12d/2026101500.gif create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_30d/2026101500.gif create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_40d/ignored.gif create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_4d/2026101500.gif create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_4d/2026101512.gif create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_4d/TasEastCoast20260922_nrt_4d.mp4 create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_4d/data.JSON create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_4d/fake.gif.html create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_4d/index.html create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_4d/index.php create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_4d/nested/ignored.gif create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_4d_backup/ignored.gif create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/missionTS/ignored.mp4 create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/missionTS/sl_TasEastCoast20260922_nrt.gif create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/navBrisbane20180306AB_4d/ignored.gif create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/navBrisbane20180306A_nrt_4d/2018031500.gif create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/navCooktown20160217_4d_nrt/2016022000.gif create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/navJervisBay20180503a_nrt_12d/2018051000.gif create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/navPerth20110626_1_4d/2011070100.gif create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/navTasEastCoast20260922_nrt_4d/2026101500.gif create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/navTasEastCoast20260922_nrt_4d/navTasEastCoast20260922_nrt_4d.mp4 create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/yearmaps/2026_sl_temp_nrt.gif create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/yearmaps_junk/ignored.gif diff --git a/ARGO/oceancurrent/README.md b/ARGO/oceancurrent/README.md index ba85c7d8..25e775ca 100644 --- a/ARGO/oceancurrent/README.md +++ b/ARGO/oceancurrent/README.md @@ -170,180 +170,52 @@ Currently, the script is adaptive to these products: ## Gliders asset indexes -The scanner discovers legacy assets under `/mnt/oceancurrent/website/gliders/`. -These proposed product IDs follow the existing `product-subproduct` convention; -registering the JSON files in `ocean-current-api` is a separate future task. -This change does not register products in the API, frontend or Elasticsearch. +The scanner discovers assets under `/mnt/oceancurrent/website/gliders/`. +Registering these proposed product IDs in `ocean-current-api` is a separate +future task; this change does not integrate Gliders into the API or frontend. -| Proposed product ID | JSON path relative to the website root | Supported assets | +| Product ID | JSON path relative to the website root | Assets | | --- | --- | --- | | `gliders-mission` | `/gliders/gliders-mission.json` | Standard mission GIFs | | `gliders-mission-video` | `/gliders/gliders-mission-video.json` | Standard mission MP4s | | `gliders-navigation` | `/gliders/gliders-navigation.json` | Navigation GIFs | -| `gliders-navigation-video` | `/gliders/gliders-navigation-video.json` | Navigation MP4s, when available | +| `gliders-navigation-video` | `/gliders/gliders-navigation-video.json` | Navigation MP4s | | `gliders-ts` | `/gliders/gliders-ts.json` | GIFs directly under `missionTS/` | | `gliders-yearmaps` | `/gliders/gliders-yearmaps.json` | GIFs directly under `yearmaps/` | -Mission directories begin with an ASCII letter, followed by letters, digits, -underscores or hyphens and an eight-digit mission date. The date may have a -single-letter mission suffix (for example `Brisbane20180306A` or -`JervisBay20180503a`) or an observed numeric leg `_1` / `_2` (for example -`Perth20110626_1`). The visualisation period is `_4d`, `_12d` or `_30d`. -An optional `_nrt` may occur before **or** after the period, but not both: +Mission directory names start with a letter and contain letters, digits, +underscores or hyphens, followed by an eight-digit mission date. The date can +have a single-letter suffix or a `_1` / `_2` mission leg. Supported periods are +`_4d`, `_12d` and `_30d`, with optional `_nrt` before or after the period: - `TasEastCoast20260922_nrt_4d` - `Brisbane20180306A_4d` +- `JervisBay20180503a_nrt_12d` - `Perth20110626_1_4d` - `Cooktown20160217_4d_nrt` -Navigation directories use exactly the same validation with a `nav` prefix. -Anchored patterns exclude `nav*` from standard missions, unsupported periods -and unrecognised directory suffixes. Only files directly inside the matched -directories are scanned. Extensions are case insensitive; HTML, JSON and PHP -files are excluded. - -A read-only inventory on 16 October 2026 contained 2,900 directories. The -updated rules match 2,133 standard and 603 navigation directories, adding 52 -and 18 respectively. All 42 previously excluded directories linked by the -legacy catalogue now match (all 1,362 distinct linked mission directories are -covered). Those 42 names are retained as a regression fixture in -`fixtures/gliders_historical_missions.txt`. The inventory was checked locally; -no production scanner or production filesystem writes were used. - -In addition to the supported `missionTS/` and `yearmaps/`, 162 directories remain -excluded deliberately: - -| Directory category | Count | Reason | -| --- | ---: | --- | -| `_tsfoa` | 126 | Separate scientific variant, outside the canonical products | -| `_vtsfc` | 14 | Separate scientific variant | -| `_vtTsS` / `_vtsTS` | 10 | Separate scientific variants | -| `_10d` | 3 | Outside the requested 4/12/30-day periods | -| `missionTS_*` / `yearmaps_*` | 9 | Development, alternate or junk folders; only exact canonical folders are enabled | - -Each mission directory is preserved in `path` and used verbatim as `region`. -This is a mission/version identifier, not a geographic region parsed from the -name. Period and NRT status remain encoded in that identifier. `depth` is -always null. T-S and annual-map records have null `region` and `depth` because -the scanner does not extract metadata from their filenames. For example, -`gliders-mission.json` can contain: +Navigation directories use the same format with a `nav` prefix and are excluded +from standard mission indexes. Only files directly inside matching directories +are indexed. GIF and MP4 extensions are case insensitive; HTML, JSON, PHP, +unsupported periods, scientific variant suffixes and auxiliary folders are excluded. +Missing periods or animations are allowed. An index is written only when matching +assets exist; the existing scanner does not remove stale indexes when assets disappear. -```json -[ - { - "path": "/gliders/TasEastCoast20260922_nrt_4d", - "productId": "gliders-mission", - "region": "TasEastCoast20260922_nrt_4d", - "depth": null, - "files": [{"name": "2026101500.gif"}] - } -] -``` - -Identical filenames in different missions remain in separate records. Missing -periods or animations are simply absent; no complete set of periods is assumed. -The existing runner writes an index only when matching assets exist. It does -not remove an old index if all matching assets subsequently disappear. - -### Scheduled scanner impact +Each mission directory is preserved in `path` and used verbatim as `region`, +so identical filenames from different missions remain distinguishable. `depth` +is null; T-S and annual-map records also have null `region`. Example record: -After deployment, the existing scheduled scanner automatically processes six -additional configurations, appended after all existing products. No schedule -or scientific generation changes are required. `save_in_product_folder` writes -up to six uniquely named indexes under `gliders/`, replacing those indexes on -subsequent runs. It does not overwrite other product indexes or source assets. - -Each configuration lists the website root and `gliders/`, but only opens -matching asset directories, with a maximum directory depth of two. Mission -folders are listed twice (GIF and MP4); asset contents are never read. -Memory scales with matching paths and the generated records for a configuration. -The existing JSON grouping searches records linearly, so many missions and -frames can also increase generation time. No shared scanner behavior was changed. - -The opt-in local benchmark is reproducible with: - -```bash -TMPDIR=/var/tmp python3 ARGO/oceancurrent/benchmark_gliders.py +```json +{ + "path": "/gliders/TasEastCoast20260922_nrt_4d", + "productId": "gliders-mission", + "region": "TasEastCoast20260922_nrt_4d", + "depth": null, + "files": [{"name": "2026101500.gif"}] +} ``` -Use a local temporary filesystem with capacity for about 2.4 million directory -entries. Each scenario uses a fresh subprocess and runs all 48 configurations -through `main()`, with existing development fixtures plus synthetic Gliders. -The no-Gliders baseline runs the same configurations with development fixtures -only. Fixture creation and cleanup are excluded from timing and worker peak RSS. -Monitoring/metadata requests are disabled. No production scanner is invoked. - -Both Gliders scenarios contain 2,133 standard and 603 navigation directories, -including historical naming forms and incomplete periods. Each GIF has a paired -ignored HTML file; every directory also has ignored PHP/JSON files. Twenty -percent of directories lack MP4s. There are 760 T-S and 210 annual GIFs. -The deterministic frame distributions repeat every 100 directories: - -- Representative: 65% have 24 GIFs, 25% have 100, 8% have 352, and 2% have 913. -- Higher load: 25% have 100 GIFs, 50% have 352, and 25% have 913. - -These are illustrative distributions using observed counts, not a census of -production frames. The higher-load case is a stress scenario, not an absolute -upper bound. Synthetic files are empty hard links; only names and metadata are -used by the scanner. Measurements on local disk with warm filesystem caches -and no CPU throttle, on 16 October 2026: - -| Measurement | No Gliders baseline | Representative | Higher load | -| --- | ---: | ---: | ---: | -| Total scanner wall time, all 48 configurations | 0.009 s | 11.66 s | 58.91 s | -| Process CPU time | 0.009 s | 11.66 s | 58.86 s | -| Gliders scanning | 0.0005 s | 3.67 s | 17.52 s | -| Gliders JSON generation | 0 s | 7.92 s | 41.13 s | -| Peak process RSS | 31.3 MiB | 262.1 MiB | 1,167.9 MiB | -| Matched Gliders assets | 0 | 238,976 | 1,167,749 | -| Generated Gliders JSON size | 0 | 16.97 MiB | 78.97 MiB | - -| Index | Representative assets | Higher-load assets | Representative JSON | Higher-load JSON | -| --- | ---: | ---: | ---: | ---: | -| `gliders-mission` | 183,534 | 906,741 | 13,284,209 bytes | 63,908,699 bytes | -| `gliders-mission-video` | 1,706 | 1,706 | 504,447 bytes | 504,447 bytes | -| `gliders-navigation` | 52,284 | 257,850 | 3,788,006 bytes | 18,177,626 bytes | -| `gliders-navigation-video` | 482 | 482 | 147,354 bytes | 147,354 bytes | -| `gliders-ts` | 760 | 760 | 52,493 bytes | 52,493 bytes | -| `gliders-yearmaps` | 210 | 210 | 14,548 bytes | 14,548 bytes | - -The observed existing production baseline was 657 seconds and approximately -1,135 MiB peak RSS, processing about 5.8 million assets. Live cron runs every -two hours with a 50% CPU limit; about 6.7 GB RAM was available at inspection. -The local total times above include only development fixtures for existing -products, **not** the full production workload. As an illustrative allowance, -doubling the added local wall time for the CPU limit gives about 23 seconds -representative / 118 seconds higher load. Adding those to the production -baseline gives approximately 680 / 775 seconds (11.3 / 12.9 minutes), increases -of 3.5% / 17.9%, within the two-hour interval. This scaling is not a measured -production prediction: hardware, cache state, hard-linked metadata and concurrent -filesystem I/O differ. Even budgeting both the old and higher-load memory peaks -additively is roughly 2.3 GiB, below the previously observed available RAM. - -JSON generation accounts for about 68–70% of added runtime. Grouping currently -searches earlier records for each file, costing O(files × directory records). -This is measurable overhead but does not justify a shared scanner change for -these measured workloads and scheduling headroom. If real growth makes it a -bottleneck, the smallest separate optimisation is a dictionary keyed by -`(region, path)` while retaining the existing output list, record order and JSON -schema. No shared scanning or grouping functions were changed here. - -The measured scenarios support deployment without a resource blocker. Check -duration and memory logs after the first normal scheduled run; production I/O -cost and future growth are still uncertainties. Merging activates the new -configurations on the next run because cron downloads the script from `master`. -No cron, deployment or scientific pipeline changes are included. - -Gliders permission, read or write failures use existing per-configuration error -handling: other configurations continue, but the job ultimately exits nonzero -and may send the existing monitoring notification. Missing Gliders directories -produce no matches and no writes. Resource exhaustion or overlapping scheduled -runs remain possible if the real workload exceeds available headroom; appending -Gliders and limiting traversal reduce interference but do not isolate resources. - -Run the local OceanCurrent tests (all fixtures are copied or built in temporary -directories) with: - -```bash -python3 -m unittest discover -s ARGO/oceancurrent -p 'test_*.py' -``` +Merging activates these configurations on the next scheduled run because the +production job downloads the scanner from `master`. Gliders runs after existing +products and writes up to six indexes under `gliders/`; check the existing runtime +and memory logs after deployment. No cron or deployment changes are required. diff --git a/ARGO/oceancurrent/benchmark_gliders.py b/ARGO/oceancurrent/benchmark_gliders.py deleted file mode 100644 index 553de3ab..00000000 --- a/ARGO/oceancurrent/benchmark_gliders.py +++ /dev/null @@ -1,108 +0,0 @@ -"""Opt-in local benchmark; creates synthetic assets only in a temporary directory. - -Run: TMPDIR=/var/tmp python3 ARGO/oceancurrent/benchmark_gliders.py -Allow space for approximately 2.4 million directory entries in the stress fixture. -Fixture creation/cleanup are excluded from worker timing and peak RSS. -""" - -import json -import logging -import os -from pathlib import Path -import resource -import shutil -import subprocess -import sys -import tempfile -import time - - -def build_fixture(root, scenario): - source = Path(__file__).parent / "tests/mnt/oceancurrent/website" - shutil.copytree(source, root, dirs_exist_ok=True) - if scenario == "baseline": - return - # Deterministic, illustrative distributions, not a production census. - distribution = ([24] * 65 + [100] * 25 + [352] * 8 + [913] * 2 - if scenario == "representative" - else [100] * 25 + [352] * 50 + [913] * 25) - for prefix, count in (("", 2133), ("nav", 603)): - for index in range(count): - period = (4, 12, 30)[index % 3] - # Includes suffixes, alternate NRT placement, missing periods/videos. - mission = f"{prefix}Mission{index}20260922" - ending = (f"A_nrt_{period}d", f"_1_{period}d", - f"_{period}d_nrt", f"_{period}d")[index % 4] - directory = root / "gliders" / (mission + ending) - directory.mkdir(parents=True) - seed = directory / "2026000000.gif" - seed.touch() - for frame in range(distribution[index % 100]): - # Hard links avoid millions of inodes; every name is a real file. - if frame: - os.link(seed, directory / f"{2026000000 + frame}.gif") - os.link(seed, directory / f"{2026000000 + frame}.html") - if index % 5: - os.link(seed, directory / f"{directory.name}.mp4") - for name in ("index.php", "data.json"): - os.link(seed, directory / name) - for folder, count in (("missionTS", 760), ("yearmaps", 210)): - directory = root / "gliders" / folder - directory.mkdir() - for index in range(count): - (directory / f"figure{index}.gif").touch() - - -def measure(root): - import oceancurrent_file_server_api as scanner - - logging.disable(logging.CRITICAL) - scanner.OCEAN_CURRENT_FILE_ROOT_PATH = str(root) - scanner.OC_API_ENDPOINT = None # No metadata or monitoring requests. - metrics = {"gliders_scan_seconds": 0, "gliders_json_seconds": 0, - "matched_files": {}, "json_bytes": {}} - scan, save = scanner.scan_files_from_config, scanner.save_result_as_json - - def timed_scan(parent_directory, config): - started = time.perf_counter() - files = scan(parent_directory, config) - if config["productId"].startswith("gliders-"): - metrics["gliders_scan_seconds"] += time.perf_counter() - started - metrics["matched_files"][config["productId"]] = len(files) - return files - - def timed_save(files, config, parent_directory): - started = time.perf_counter() - save(files, config, parent_directory) - if config["productId"].startswith("gliders-"): - metrics["gliders_json_seconds"] += time.perf_counter() - started - output = parent_directory / "gliders" / f'{config["productId"]}.json' - metrics["json_bytes"][config["productId"]] = output.stat().st_size - - scanner.scan_files_from_config = timed_scan - scanner.save_result_as_json = timed_save - started, cpu_started = time.perf_counter(), time.process_time() - scanner.main() # All 48 configurations, including existing development fixtures. - metrics["total_seconds"] = time.perf_counter() - started - metrics["cpu_seconds"] = time.process_time() - cpu_started - rss = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss - metrics["peak_rss_mib"] = rss / (1024 ** 2 if sys.platform == "darwin" else 1024) - print(json.dumps(metrics)) - - -if __name__ == "__main__": - if len(sys.argv) == 3 and sys.argv[1] == "--worker": - measure(Path(sys.argv[2])) - else: - for scenario in ("baseline", "representative", "higher-load"): - print(f"Preparing {scenario} temporary fixture...", flush=True) - with tempfile.TemporaryDirectory(prefix="gliders-benchmark-") as temporary: - root = Path(temporary) / "website" - build_fixture(root, scenario) - result = subprocess.run( - [sys.executable, str(Path(__file__).resolve()), "--worker", str(root)], - check=True, capture_output=True, text=True, - ) - # The scanner can print a logging fallback warning before JSON. - metrics = json.loads(result.stdout.splitlines()[-1]) - print(json.dumps({"scenario": scenario, **metrics}), flush=True) diff --git a/ARGO/oceancurrent/fixtures/gliders_historical_missions.txt b/ARGO/oceancurrent/fixtures/gliders_historical_missions.txt deleted file mode 100644 index 1d6e8a34..00000000 --- a/ARGO/oceancurrent/fixtures/gliders_historical_missions.txt +++ /dev/null @@ -1,42 +0,0 @@ -Brisbane20180306A_12d -Brisbane20180306A_30d -Brisbane20180306A_4d -Brisbane20180306A_nrt_12d -Brisbane20180306A_nrt_30d -Brisbane20180306A_nrt_4d -Brisbane20180306D_12d -Brisbane20180306D_30d -Brisbane20180306D_4d -Brisbane20180306D_nrt_12d -Brisbane20180306D_nrt_30d -Brisbane20180306D_nrt_4d -Brisbane20180320A_12d -Brisbane20180320A_30d -Brisbane20180320A_4d -Brisbane20180320A_nrt_12d -Brisbane20180320A_nrt_30d -Brisbane20180320A_nrt_4d -Brisbane20180320B_12d -Brisbane20180320B_30d -Brisbane20180320B_4d -Brisbane20180320B_nrt_12d -Brisbane20180320B_nrt_30d -Brisbane20180320B_nrt_4d -JervisBay20180503a_12d -JervisBay20180503a_30d -JervisBay20180503a_4d -Perth20110626_1_12d -Perth20110626_1_30d -Perth20110626_1_4d -Perth20110626_2_12d -Perth20110626_2_30d -Perth20110626_2_4d -TimorSea20181017a_12d -TimorSea20181017a_30d -TimorSea20181017a_4d -TimorSea20181017b_12d -TimorSea20181017b_30d -TimorSea20181017b_4d -TwoRocks20180503a_12d -TwoRocks20180503a_30d -TwoRocks20180503a_4d diff --git a/ARGO/oceancurrent/test_oceancurrent_file_server_api.py b/ARGO/oceancurrent/test_oceancurrent_file_server_api.py index 8e81f0e6..10fcd64c 100644 --- a/ARGO/oceancurrent/test_oceancurrent_file_server_api.py +++ b/ARGO/oceancurrent/test_oceancurrent_file_server_api.py @@ -6,11 +6,7 @@ import unittest.mock from unittest.mock import patch, MagicMock, mock_open -from pathlib import Path - -from oceancurrent_file_server_api import ( - main, FILE_PATH_CONFIG, scan_files_from_config, save_result_as_json, -) +from oceancurrent_file_server_api import main class TestFileServerAPI(unittest.TestCase): @@ -482,6 +478,137 @@ def prepare_test_cases(self): ] } ], + "gliders-mission": [ + { + "path": "/gliders/TasEastCoast20260922_nrt_4d", + "productId": "gliders-mission", + "region": "TasEastCoast20260922_nrt_4d", + "depth": None, + "files": [{"name": "2026101500.gif"}, {"name": "2026101512.gif"}] + }, + { + "path": "/gliders/TasEastCoast20260922_nrt_12d", + "productId": "gliders-mission", + "region": "TasEastCoast20260922_nrt_12d", + "depth": None, + "files": [{"name": "2026101500.gif"}] + }, + { + "path": "/gliders/TasEastCoast20260922_nrt_30d", + "productId": "gliders-mission", + "region": "TasEastCoast20260922_nrt_30d", + "depth": None, + "files": [{"name": "2026101500.gif"}] + }, + { + "path": "/gliders/TasEastCoast20260811_4d", + "productId": "gliders-mission", + "region": "TasEastCoast20260811_4d", + "depth": None, + "files": [{"name": "2026101500.gif"}] + }, + { + "path": "/gliders/Brisbane20180306A_4d", + "productId": "gliders-mission", + "region": "Brisbane20180306A_4d", + "depth": None, + "files": [{"name": "2018031500.gif"}] + }, + { + "path": "/gliders/Perth20110626_1_4d", + "productId": "gliders-mission", + "region": "Perth20110626_1_4d", + "depth": None, + "files": [{"name": "2011070100.gif"}] + }, + { + "path": "/gliders/Cooktown20160217_4d_nrt", + "productId": "gliders-mission", + "region": "Cooktown20160217_4d_nrt", + "depth": None, + "files": [{"name": "2016022000.gif"}] + }, + { + "path": "/gliders/JervisBay20180503a_nrt_12d", + "productId": "gliders-mission", + "region": "JervisBay20180503a_nrt_12d", + "depth": None, + "files": [{"name": "2018051000.gif"}] + }, + ], + "gliders-mission-video": [ + { + "path": "/gliders/TasEastCoast20260922_nrt_4d", + "productId": "gliders-mission-video", + "region": "TasEastCoast20260922_nrt_4d", + "depth": None, + "files": [{"name": "TasEastCoast20260922_nrt_4d.mp4"}] + }, + ], + "gliders-navigation": [ + { + "path": "/gliders/navTasEastCoast20260922_nrt_4d", + "productId": "gliders-navigation", + "region": "navTasEastCoast20260922_nrt_4d", + "depth": None, + "files": [{"name": "2026101500.gif"}] + }, + { + "path": "/gliders/navBrisbane20180306A_nrt_4d", + "productId": "gliders-navigation", + "region": "navBrisbane20180306A_nrt_4d", + "depth": None, + "files": [{"name": "2018031500.gif"}] + }, + { + "path": "/gliders/navPerth20110626_1_4d", + "productId": "gliders-navigation", + "region": "navPerth20110626_1_4d", + "depth": None, + "files": [{"name": "2011070100.gif"}] + }, + { + "path": "/gliders/navCooktown20160217_4d_nrt", + "productId": "gliders-navigation", + "region": "navCooktown20160217_4d_nrt", + "depth": None, + "files": [{"name": "2016022000.gif"}] + }, + { + "path": "/gliders/navJervisBay20180503a_nrt_12d", + "productId": "gliders-navigation", + "region": "navJervisBay20180503a_nrt_12d", + "depth": None, + "files": [{"name": "2018051000.gif"}] + }, + ], + "gliders-navigation-video": [ + { + "path": "/gliders/navTasEastCoast20260922_nrt_4d", + "productId": "gliders-navigation-video", + "region": "navTasEastCoast20260922_nrt_4d", + "depth": None, + "files": [{"name": "navTasEastCoast20260922_nrt_4d.mp4"}] + }, + ], + "gliders-ts": [ + { + "path": "/gliders/missionTS", + "productId": "gliders-ts", + "region": None, + "depth": None, + "files": [{"name": "sl_TasEastCoast20260922_nrt.gif"}] + }, + ], + "gliders-yearmaps": [ + { + "path": "/gliders/yearmaps", + "productId": "gliders-yearmaps", + "region": None, + "depth": None, + "files": [{"name": "2026_sl_temp_nrt.gif"}] + }, + ], } def load_and_normalize_json(self, file_path): @@ -578,6 +705,20 @@ def test_file_structure_explorer(self): self.verify_json("fishSOOP-profiles", "fishsoop", "fishSOOP-profiles") self.verify_json("fishSOOP-quarterlyAnomalies", "fishsoop", "fishSOOP-quarterlyAnomalies") self.verify_json("fishSOOP-depthAnomalies", "fishsoop", "fishSOOP-depthAnomalies") + # Exact Gliders paths exclude navigation from standard indexes and + # unrelated directories. Exact filenames also exclude HTML/JSON/PHP. + for product_id in ("gliders-mission", "gliders-mission-video", + "gliders-navigation", "gliders-navigation-video", + "gliders-ts", "gliders-yearmaps"): + self.verify_json(product_id, "gliders", product_id) + output = os.path.join(self.file_test_dir, "gliders", f"{product_id}.json") + actual = self.load_and_normalize_json(output) + expected = self.prepare_test_cases()[product_id] + self.assertEqual( + {item["path"]: sorted(f["name"] for f in item["files"]) for item in actual}, + {item["path"]: sorted(f["name"] for f in item["files"]) for item in expected}, + f"Unexpected Gliders filenames in {product_id}", + ) # Verify the fishsoop finder maps folder is excluded by the region whitelist fishsoop_json_path = os.path.join(self.file_test_dir, "fishsoop", "fishSOOP-profiles.json") fishsoop_regions = {item["region"] for item in self.load_and_normalize_json(fishsoop_json_path)} @@ -590,147 +731,6 @@ def test_file_structure_explorer(self): not_existed_path = os.path.join(self.file_test_dir, "timeseries", "currentMetersCalendar-48.json") self.assertFalse(os.path.exists(not_existed_path)) - - -class TestGlidersConfiguration(unittest.TestCase): - """Build representative assets only inside a temporary website root.""" - - def setUp(self): - temporary = tempfile.TemporaryDirectory() - self.addCleanup(temporary.cleanup) - self.root = Path(temporary.name) - self.configs = [c for c in FILE_PATH_CONFIG - if c["productId"].startswith("gliders-")] - self.expected = {c["productId"]: [] for c in self.configs} - for navigation in (False, True): - category = "navigation" if navigation else "mission" - for mission, periods in (("TasEastCoast20260922_nrt", (4, 12, 30)), - ("TasEastCoast20260811", (4,))): - for period in periods: - directory = ("nav" if navigation else "") + f"{mission}_{period}d" - for extension, suffix in (("gif", ""), ("mp4", "-video")): - # Older missions deliberately have no animation. - if extension == "mp4" and "nrt" not in mission: - continue - name = "2026101500.gif" if extension == "gif" else f"{directory}.mp4" - self.add_asset(directory, name) - self.expected[f"gliders-{category}{suffix}"].append({ - "path": f"/gliders/{directory}", - "productId": f"gliders-{category}{suffix}", - "region": directory, "depth": None, - "files": [{"name": name}], - }) - for name in ("index.html", "data.json", "index.php", "fake.gif.html"): - self.add_asset(directory, name) - for category, directory, name in ( - ("ts", "missionTS", "sl_TasEastCoast20260922_nrt.gif"), - ("yearmaps", "yearmaps", "2026_sl_temp_nrt.gif")): - self.add_asset(directory, name) - self.add_asset(directory, "index.php") - self.add_asset(directory, "ignored.mp4") - self.expected[f"gliders-{category}"].append({ - "path": f"/gliders/{directory}", "productId": f"gliders-{category}", - "region": None, "depth": None, "files": [{"name": name}], - }) - for directory in ("TasEastCoast20260922_nrt_4d_backup", - "TasEastCoast20260922_nrt_40d", "unrelated", - "prefixnavTasEastCoast20260922_nrt_4d_extra"): - self.add_asset(directory, "2026101500.gif") - self.add_asset("TasEastCoast20260922_nrt_4d/nested", "ignored.gif") - sibling = self.root / "gliders_backup" / "TasEastCoast20260922_nrt_4d" - sibling.mkdir(parents=True) - (sibling / "ignored.gif").touch() - - def add_asset(self, directory, name): - target = self.root / "gliders" / directory / name - target.parent.mkdir(parents=True, exist_ok=True) - target.touch() - - def test_exact_indexes_and_repeat_scan(self): - self.assertEqual(len(self.configs), 6) - sentinel = self.root / "gliders" / "unrelated.json" - sentinel.write_text("preserve me") - for _ in range(2): # Generated indexes must not feed back into the next scan. - for config in self.configs: - with self.subTest(product=config["productId"]): - files = scan_files_from_config(self.root, config) - save_result_as_json(files, config, self.root) - output = self.root / "gliders" / f'{config["productId"]}.json' - actual = json.loads(output.read_text()) - self.assertEqual(sorted(actual, key=lambda r: r["path"]), - sorted(self.expected[config["productId"]], - key=lambda r: r["path"])) - self.assertEqual(sentinel.read_text(), "preserve me") - self.assertEqual(set(self.root.rglob("*.json")), - {self.root / "gliders" / f'{c["productId"]}.json' - for c in self.configs} - | {sentinel} - | set(self.root.glob("gliders/*/data.json"))) - - def test_historical_missions(self): - # All 42 directories linked by the legacy catalogue and missed originally. - catalogue = Path(__file__).parent / "fixtures/gliders_historical_missions.txt" - names = catalogue.read_text().splitlines() - self.assertEqual(len(names), 42) - # Other confirmed production names: NRT after the period, and suffix + NRT. - names += ["Cooktown20160217_4d_nrt", "Cooktown20160217_12d_nrt", - "Portland20160128_4d_nrt", "JervisBay20180503a_nrt_12d"] - for navigation in (False, True): - category = "navigation" if navigation else "mission" - for name in names: - directory = ("nav" if navigation else "") + name - self.add_asset(directory, "2026101500.gif") - self.add_asset(directory, f"{directory}.mp4") - for suffix, extension in (("", ".gif"), ("-video", ".mp4")): - product = f"gliders-{category}{suffix}" - config = next(c for c in self.configs if c["productId"] == product) - save_result_as_json(scan_files_from_config(self.root, config), - config, self.root) - actual = json.loads((self.root / "gliders" / f"{product}.json").read_text()) - expected = self.expected[product] + [{ - "path": f"/gliders/{('nav' if navigation else '') + name}", - "productId": product, - "region": ("nav" if navigation else "") + name, - "depth": None, - "files": [{"name": "2026101500.gif" if extension == ".gif" - else f"{('nav' if navigation else '') + name}.mp4"}], - } for name in names] - self.assertEqual(sorted(actual, key=lambda r: r["path"]), - sorted(expected, key=lambda r: r["path"])) - - def test_exclude_unconfirmed_variants(self): - names = ["Brisbane20180306AB_4d", "Perth20110626_3_4d", - "Cooktown20160217_nrt_4d_nrt", "Cooktown20160217_4d_NRT", - "Mission20260922_4d_backup", "Mission20260922_4d_tsfoa", - "Mission20260922_12d_vtsfc", "Mission20260922_nrt_4d_vtTsS", - "Mission20260922_nrt_4d_vtsTS", "Mission20260922_10d", - "missionTS_sl_dev1", "yearmaps_junk"] - for prefix in ("", "nav"): - for name in names: - for extension in ("gif", "mp4"): - self.add_asset(prefix + name, "unrelated." + extension) - for config in self.configs: - files = scan_files_from_config(self.root, config) - self.assertFalse(any(f.stem == "unrelated" for f in files)) - - def test_optional_products_absent(self): - for asset in (self.root / "gliders").rglob("*.mp4"): - asset.unlink() - shutil.rmtree(self.root / "gliders/missionTS") - shutil.rmtree(self.root / "gliders/yearmaps") - for config in self.configs: - files = scan_files_from_config(self.root, config) - if config["productId"] in ("gliders-mission", "gliders-navigation"): - self.assertEqual(len(files), 4) # Three NRT periods + one older period. - else: - self.assertEqual(files, []) - - def test_absent_gliders(self): - with tempfile.TemporaryDirectory() as empty: - for config in self.configs: - self.assertEqual(scan_files_from_config(Path(empty), config), []) - - class TestMonitoringFunctions(unittest.TestCase): """Tests for EC2 monitoring and fatal log notification functions.""" diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/Brisbane20180306AB_4d/ignored.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/Brisbane20180306AB_4d/ignored.gif new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/Brisbane20180306A_4d/2018031500.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/Brisbane20180306A_4d/2018031500.gif new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/Brisbane20180306A_4d_tsfoa/ignored.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/Brisbane20180306A_4d_tsfoa/ignored.gif new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/Cooktown20160217_4d_nrt/2016022000.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/Cooktown20160217_4d_nrt/2016022000.gif new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/Cooktown20160217_nrt_4d_nrt/ignored.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/Cooktown20160217_nrt_4d_nrt/ignored.gif new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/JervisBay20180503a_nrt_12d/2018051000.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/JervisBay20180503a_nrt_12d/2018051000.gif new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/Perth20110626_1_4d/2011070100.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/Perth20110626_1_4d/2011070100.gif new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/Perth20110626_3_4d/ignored.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/Perth20110626_3_4d/ignored.gif new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260811_4d/2026101500.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260811_4d/2026101500.gif new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_12d/2026101500.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_12d/2026101500.gif new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_30d/2026101500.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_30d/2026101500.gif new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_40d/ignored.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_40d/ignored.gif new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_4d/2026101500.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_4d/2026101500.gif new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_4d/2026101512.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_4d/2026101512.gif new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_4d/TasEastCoast20260922_nrt_4d.mp4 b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_4d/TasEastCoast20260922_nrt_4d.mp4 new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_4d/data.JSON b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_4d/data.JSON new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_4d/fake.gif.html b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_4d/fake.gif.html new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_4d/index.html b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_4d/index.html new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_4d/index.php b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_4d/index.php new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_4d/nested/ignored.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_4d/nested/ignored.gif new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_4d_backup/ignored.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/TasEastCoast20260922_nrt_4d_backup/ignored.gif new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/missionTS/ignored.mp4 b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/missionTS/ignored.mp4 new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/missionTS/sl_TasEastCoast20260922_nrt.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/missionTS/sl_TasEastCoast20260922_nrt.gif new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/navBrisbane20180306AB_4d/ignored.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/navBrisbane20180306AB_4d/ignored.gif new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/navBrisbane20180306A_nrt_4d/2018031500.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/navBrisbane20180306A_nrt_4d/2018031500.gif new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/navCooktown20160217_4d_nrt/2016022000.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/navCooktown20160217_4d_nrt/2016022000.gif new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/navJervisBay20180503a_nrt_12d/2018051000.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/navJervisBay20180503a_nrt_12d/2018051000.gif new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/navPerth20110626_1_4d/2011070100.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/navPerth20110626_1_4d/2011070100.gif new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/navTasEastCoast20260922_nrt_4d/2026101500.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/navTasEastCoast20260922_nrt_4d/2026101500.gif new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/navTasEastCoast20260922_nrt_4d/navTasEastCoast20260922_nrt_4d.mp4 b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/navTasEastCoast20260922_nrt_4d/navTasEastCoast20260922_nrt_4d.mp4 new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/yearmaps/2026_sl_temp_nrt.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/yearmaps/2026_sl_temp_nrt.gif new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/yearmaps_junk/ignored.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/gliders/yearmaps_junk/ignored.gif new file mode 100644 index 00000000..e69de29b