diff --git a/demo_apps/task-limits/assess-utilization.ipynb b/demo_apps/task-limits/assess-utilization.ipynb index 12022f5..e2d00d3 100644 --- a/demo_apps/task-limits/assess-utilization.ipynb +++ b/demo_apps/task-limits/assess-utilization.ipynb @@ -20,6 +20,7 @@ "import pandas as pd\n", "import numpy as np\n", "import json\n", + "import re\n", "params = {'legend.fontsize': 8,\n", " 'axes.labelsize': 9,\n", " 'axes.titlesize':'x-large',\n", @@ -45,7 +46,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Found 16 runs\n" + "Found 14 runs\n" ] } ], @@ -74,6 +75,7 @@ " config = json.load(fp)\n", " \n", " config['path'] = path.parent\n", + " config['host'] = path.parent.name.rsplit(\"-\", 5)[0]\n", " return config" ] }, @@ -109,6 +111,7 @@ " \n", " # Get the results for each worker\n", " results = pd.read_json(path / \"results.json\", lines=True)\n", + " \n", " if len(results) == 0:\n", " return None\n", " results['worker'] = results['worker_info'].apply(lambda x: f'{x[\"hostname\"]}-{x[\"PARSL_WORKER_RANK\"]}')\n", @@ -146,9 +149,16 @@ "execution_count": 7, "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "No artists with labels found to put in legend. Note that artists whose label start with an underscore are ignored when legend() is called with no argument.\n" + ] + }, { "data": { - "image/png": "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", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAVQAAAClCAYAAAADKwW9AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy81sbWrAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAaKklEQVR4nO3de1BU5/0G8GdXlgWUZbkJFQXFWzREQ82oKDchUbFiTEsxagw603oLUUMwU3WqIFVsEy1aTVObEcWIl9ai0apUjCBWFIuXihhFRRFRCurCorjAsr8/HPbnhot7ObC75PnM7MzuOWfP+b4keXL2vOe8r0ij0WhAREQmE5u7ACKiroKBSkQkEAYqEZFAGKhERAJhoBIRCYSBSkQkEAYqEZFAGKhERAKxMXcBHaGpqQnl5eVwdHSESCQydzlEZOE0Gg2USiV69eoFsdj488wuGajl5eXo06ePucsgIitz79499O7d2+jvd8lAdXR0BPDijyOTycxcDRFZupqaGvTp00ebHcYyS6AqlUq8/fbbuHr1Ks6ePQs/Pz/s3bsXKSkpsLe3x44dO9CnTx8UFRVh7ty5UKvVSEpKwttvv63X/pt/5stkMgYqEenN1EuEZumUsre3x+HDhxEVFQUAaGhowIYNG5CTk4OkpCQkJSUBAJYvX47U1FRkZmZi5cqV5iiViEhvZglUGxsbuLu7az8XFxfj9ddfh62tLcaOHYsrV64AAB48eICBAwdCJpPB1dUVVVVVre5PpVKhpqZG50VEpFar8fz58xYvtVrdIceziGuoCoVC56d5c2NfHlnQyckJjx8/hpubW4vvJycnIzExseMLJSKrUVtbi7KyMrQ2QqlIJELv3r3Ro0cPQY9pEYHq7Oysc1bZrVs3ANC5fUGhUMDFxaXV7y9btgxxcXHaz80XmInox0mtVqOsrAwODg5wd3fXuTaq0WhQWVmJsrIyDBw4UJs3QrCIQB0wYACKiopQX1+P8+fPY9iwYQAAT09PFBcXw8PDo82zUwCQSqWQSqWdWTIRWbCGhgZoNBq4u7vD3t6+xXp3d3fcuXMHDQ0NXSNQJ02ahEuXLuH69euYN28elixZgpCQENjZ2SEtLQ0AsHbtWsyZMwdqtRqrV682V6lEZKXa6rXvqAd+zBaoR44cabHs/fff1/k8dOhQnD59urNKIiIyCZ/lJyISiMFnqE+fPkVhYSGePHkCZ2dn+Pn5oXv37h1RGxGRSdqag7Sj5ibVO1APHTqEP/3pT8jOzoadnR1kMhlqamrw/PlzhISEIDY2Fu+++26HFElEZAiJRAKRSITKyso2e/lFIhEkEomgxxXpM410cHAwJBIJPvzwQ4SHh+sMHlBWVobvvvsOaWlpqK+vx6lTpwQt0Bg1NTVwcnJCdXU1Hz0l+pEy5D5UoTJDr0A9c+YMxowZ88qd5eXlISAgwOhihMJAJSLgxf2oDQ0NLZZLJBKd26U6NVCtDQOViAwhVGYY3ctfW1uL5cuXY/LkyVi8eDEePnxodBFERF2B0YEaGxsLqVSKRYsWQSqVIjo6Wsi6iIisjt69/AkJCVixYoW2V6ykpATbt28HAISGhqJnz54dUiARkbXQ+wzVzs4Oo0aN0vbih4aGYvz48VixYgXCw8MRGRnZYUUSEVkDgzqlbt++jY8++gheXl74/PPPkZ+fj8uXL8PX1xc///nPTZrcSkjslCIiQ5ilU8rX1xdHjx5FWFgYQkJC8OjRI3z22WeIioqymDAlIjIXg1IwIyMDX3zxBVxdXZGTk4PvvvsOERERuHPnjsmFNDU1ISYmBkFBQQgODsatW7ewd+9eBAQEICwsDPfu3TP5GEREHUnvTqk5c+aguLgYQUFB2gnzvv76a+Tk5OAXv/gFpk2bhs8++8zoQi5dugSVSoXc3FwcP34cmzdvxpkzZ5Cbm4vz588jKSkJW7duNXr/REQdTe8z1IMHD+LkyZNITk5GVlYWDhw4AAAICQlBXl4e6urqTCqk+XFWjUYDhUIBd3f3VueZIiKyVHqfoQ4dOhQJCQkIDQ1FVlYW/Pz8tOtsbW2xatUqkwpxc3ODWCzGkCFDoFKpsGvXLvzvf//Trm9vUi2VSgWVSqX9zEn6iMgc9D5D3bt3L6qrq/HHP/4RALB582ZBC8nMzIS9vT2+//577N+/Hxs3bmx1nqnWJCcnw8nJSfvifFJEZA56n6F6eXkJHqI/5OzsDACQy+WoqqrC3bt3W8wz1RpO0kdElkCvQM3MzMSECRME264148ePx86dOxESEgKVSoUNGzagtLS0xTxTreEkfURkCfS6sT8iIgJVVVWYM2cOwsLCMHjwYIhEImg0Gly/fh0nT55Eamoq3NzcWp0rqrPxxn4iMkSnD9938uRJfPnllzh27Bjq6urg4OCAZ8+ewcHBARMmTMDChQsxbtw4owsREgOViAxhtvFQGxsbUVxcrJ1TauDAgbCxMdvkqa1ioBKRIYTKDIOT0MbGBkOGDDH6gEREXRUfwCciEggDlYhIIAxUIiKBMFCJiARicKeUWq3Grl27UFBQAKVSqbNu27ZtghVGRGRtDA7UX/3qV8jKykJERAScnJw6oiYiIqtkcKAePHgQRUVF8PT07Ih6iIislsHXUF1dXeHo6NgRtRARWTWDA3XVqlX49a9/jeLiYtTU1Oi8iIh+zAx+9PTlyfhEIhGAF6Psi0SidgeB7kx89JSIDGG2R09LSkqMPtirZGdnIykpCY2NjYiLi8Pz58+RkpICe3t77Nixg2OcEpFFMzhQfXx8tO+rqqrg5uYmSCHPnz/H+vXrcfToUdja2qKhoQGBgYGcpI+IrIbB11CfPXuGefPmwcHBAR4eHnBwcMD8+fPx9OlTkwo5c+YM7O3tERkZiffeew/nz5/Xe5I+lUrF67lEZHYGB+qnn36KGzdu4MSJEygvL8eJEydQXFyM+Ph4kwqpqKhASUkJDh06hLlz5yIhIUHnWkZ712c5pxQRWQKDA/Xbb7/F/v37ERAQAA8PDwQEBGDfvn04ePCgSYXI5XIEBgbC1tYWYWFhuHjxot6T9C1btgzV1dXa171790yqhYjIGAYHqkaj0enpB170/Bt4s0ALI0eORFFREQDg4sWLGD9+PIqKilBfX49///vf7U7SJ5VKIZPJdF5ERJ3N4E6pyZMnIyoqCuvWrYOPjw/u3LmDFStWIDIy0qRCXF1dMWXKFAQHB0MsFmPbtm3Iz8/Xa5I+IiJLYPB9qLW1tVi0aBF2796N+vp62NraYvr06di4caPFPEHF+1CJyBBmm1OqmUajQWVlJdzd3bU3+FsKBioRGcJsN/Y3E4lE6Nmzp9EHJiLqavQKVG9vb5SWlgIAnJ2d2zwjffz4sXCVERFZGb0CNT09Xfv+wIEDHVULEZFV0ytQAwMDte/Ly8sxffr0Ftvs2bNHuKqIiKyQwZ1SMpms1Uc7XVxcLOYnPzuliMgQnd4p1RyiGo0GSqVS50b+W7duQSKRGF0EEVFXoHegyuVybWeUXC7XWScWi7Fq1SpBCyMisjZ6B2pJSQk0Gg1GjRqF/Px87XKxWAx3d3fY2dl1SIFERNZC70BtHge1oqKiw4ohIrJmRt3Yn5eXh+zsbFRVVelcS92wYYNghRERWRuDR5vasmULwsPDkZ+fjy1btqCkpAR/+ctf8PDhw46oj4jIahgcqCkpKTh69CgyMjJgb2+PjIwM/O1vf4NUKhWkoN27d8Pd3R0AsHfvXgQEBCAsLIxjnBKRxTPpPlQXFxc8evQIAODm5qZ9b6ympib88pe/RElJCc6dO6czp9SOHTv0nlOK96ESkSGEygyDz1A9PT1RXl4OAOjXrx+ys7Nx9erVFoNOGyM9PR1RUVEQi8UoLi7We04pIiJLYHAKLliwAOfOnQMAxMXFYfz48fD398fChQtNKkStVmPfvn2YNm0aAEChUOg9pxQn6SMiS2BwL/8nn3yifT9z5kwEBwejtrYWQ4YMMamQb775BtHR0dozXWdnZ73nlEpOTkZiYqJJxyciMpXBZ6irV69GYWGh9nOfPn0wZMgQrFu3zqRCioqKkJaWhokTJ6K4uBhbt27Ve04pTtJHRJbA4E4pGxsbyOVy7Ny5ExEREdrlbQ2aYoy33noL//nPf7Bnzx5s3LhRO6eUvtNDs1OKiAxhtilQHB0dcejQIURHR2PlypWIjY3VLlcqlUYXIiQGKhEZwmy9/CKRCKGhocjNzcWmTZuwaNEiNDU1GV0AEVFXYfS9ToMHD0ZeXh4uX76MyMhIhioR/egZHKiDBg3Svnd1dUVWVhZcXFxQV1cnaGFERNbG6GmkLRmvoRKRITp1xP4jR45g0qRJAIBvv/221W1EIhEiIyONLoSIyNrpdYbq5+envfe0X79+re9IJMLt27eFrc5IPEMlIkN06hnqyzfyl5SUGH0wIqKuzPQRTYiICICeZ6j+/v7aCfrac+HCBZMLIiKyVnoF6pIlSzq4DCIi66dXoMbExHR0HUREVk+w26YAYMqUKcJURURkhXjbFBH96HW526YKCgqwZMkSiMVieHh4YNeuXfjHP/6BlJQU2NvbY8eOHXoP30dEZA4G3zb1s5/9rNXlpv7c9/LyQmZmJnJycjBgwAAcOHAAGzZsQE5ODpKSkpCUlGTS/omIOprBgZqbm9vq8tOnT5tUiKenJxwcHAAAEokEN27c0HuSPs4pRUSWQO85pTZt2gQAaGho0L5vduvWLXh6egpSUGlpKbKysrB27VpUVlZql7c3SR/nlCIiS6B3oGZkZAB4EajN7wFor3lu377d5GJqamowa9YspKamQq1W6z1J37JlyxAXF6ezH15vJaLOpnegnjx5EgAQHx+PL774QvBC1Go1Zs6ciZUrV2LQoEFoaGjQTtJ3/vz5difpk0qlkEqlgtdERGQIvcdDbe26pEQigb29vSCF7N69G7GxsXjjjTcAAAsWLIBGo+EkfUTU4Tp9kj6xWNzq8/xubm744IMPsGbNGtjZ2RldiJAYqERkiE69DxVo/f7ThoYG3Lx5E4mJiUhMTERycrLRhRARWTtBpkC5e/cuwsPDcfPmTSFqMhnPUInIEGabRro1Pj4+qKqqEmJXRERWS5BAvXTpEry8vITYFRGR1dL7Gmpro0w1NDSgpKQEW7ZsQXx8vKCFERFZG70DdfHixS2WSSQS+Pj4ICEhgWOmEtGPnkm9/ERE9P84SR8RkUAYqEREAmGgEhEJhIFKRCQQBioRkUCsIlDj4+MRFBSEmTNnor6+3tzlEBG1yuID9eLFi3j48CFyc3MxdOhQ/P3vfzd3SURErbL4QM3Ly8P48eMBABMnTsSZM2fMXBERUev0vrHfXBQKBXr16gUAcHJywuPHj1tso1KpoFKptJ+rq6sBtD4oNhHRDzVnhamD71l8oDo7O2sbq1Ao4OLi0mKbtibp47xSRGSIR48ewcnJyejvCzIeake6ePEi1q9fj2+++QZr1qyBr68vpk+frrPND89QFQoFfHx8UFpaatIfx9I0Tz547969LjXOK9tlXbpiu6qrq+Ht7Y0nT55ALpcbvR+LP0P19/eHp6cngoKC4O3tjaVLl7bYpq1J+pycnLrMP/CXyWQytsuKsF3WQyw2rVvJ4gMVQIfMskpEJDSL7+UnIrIWXTJQpVIpVq1a1eplAGvGdlkXtst6CNUmi++UIiKyFl3yDJWIyBwYqEREAukSgdrW4CmNjY2YPXs2goKCWp0Ty9K11a4jR45gzJgxCAwMRGxsrBkrNM6rBrtJTk7GW2+9ZYbKTNNeu/bs2YOwsDAEBwcjPz/fTBUarq021dXVYfLkyQgJCcE777zT6hOMlkypVGLUqFHo0aMHCgsLddaZkhtWH6jtDZ5y6NAh9O7dG7m5uXj27JlVjQPQXrv8/Pxw6tQpnD59Go8fP8b58+fNWKlhXjXYjVKpbPEvuDVor13l5eU4ePAgTpw4gVOnTmHkyJFmrFR/7bXp6NGj8PPzQ05ODqKjo7Fz504zVmo4e3t7HD58GFFRUS3WmZIbVh+o7Q2eYs0Dq7RXu7e3N2xsXtxCLJFItO+twav+mWzcuBEfffSROUozSXvtOnbsGKRSKd555x3MmjULtbW15irTIO21aeDAgXj27BmAF08muru7m6VGY9nY2LRZsym5YfWBqlAotE9r/HDwlPbWWTp9ai8oKEBVVRX8/f07uzyjtdeu6upqXLlyBWPGjDFXeUZrr10VFRVQKBQ4fvw4xowZg82bN5urTIO016b+/fujsLAQfn5+SEtLw9SpU81UpfBMyQ2rD9T2Bk/RZ2AVS/Wq2svKyrB48WJs377dDNUZr712paSkWOU1YaD9dsnlcowbNw4ikQhhYWEoKioyV5kGaa9NO3bsQGhoKAoLC5GYmIjVq1ebq0zBmZIbVh+oo0ePxr/+9S8AQGZmJsaOHavXOkvXXu21tbWYMWMGvvrqK6v7qdVeu27evIk1a9Zg4sSJKC4uxrp168xVpsHaa9fYsWNx6dIlAC+uS/r6+pqjRIO96r+f5qCRy+VQKBSdXV6HMSk3NF3Ap59+qgkMDNTMmDFDo1KpNHPnztVoNBpNQ0OD5sMPP9QEBgZqPv74YzNXabi22rV27VpNr169NCEhIZqQkBBNdna2mSs1TFvtetmIESPMUJlp2mvXsmXLNCEhIZqJEydqHj16ZMYqDdNWm6qrqzWTJk3ShISEaMaOHau5fv26mSs1XEREhOYnP/mJZvTo0ZrU1FRBcoNPShERCcTqf/ITEVkKBioRkUAYqEREAmGgEhEJhIFKRCQQBioRkUAYqEREAmGgUqcTiUTaJ4esydq1a1tMYS6kmpoa+Pr6orKy8pXbDRgwAFVVVR1WCxmHgUrt6tGjh/bVrVs3SKVS7eeIiIgOP/7s2bNha2sLR0dHODk5YdCgQZg/fz5KSko69Ljbt2/Hm2++qbNs+fLl2L17d4cdc/369Xjvvfde+TixTCbDrFmzsGbNmg6rhYzDQKV21dbWal9BQUH4/e9/r/189OjRTqlh4cKFUCqVqK6uRmZmJmxtbeHv749r164Ztb/GxkaBKzRdY2Mjtm7dijlz5ui1fUxMDFJTU7VD6JFlYKCSUWpra/Huu++iZ8+ecHJyQnBwMC5fvqxdf+HCBYwePRoymQxubm6IjIxsdT83btxA//799R7Srl+/fti0aRNGjx6NhIQEAEB2djbkcrnOdlOnTm2x/s9//jO8vb0REBAAAPjggw/Qq1cvyGQyjBgxAidPngTwYgCT+fPn48qVK9qz8dLSUiQkJOgMU3fz5k1MmDABLi4u6N+/P1JSUrTrms9wk5KS0LNnT3h4eOis/6H8/Hyo1Wr4+flplx0/fhzDhg2Do6MjPDw8sGDBAu26vn37wtXVFTk5OXr93ahzMFDJKE1NTZgxYwZKSkpQUVEBf39/REdHo3loiNjYWERGRkKhUOD+/ftYunRpi33k5+cjLCwMycnJBg/bFxUVhezsbL23VyqVuHz5Mr7//nttCIWHh+PatWt49OgR3n//fURFRUGpVMLf3x9fffUV3njjDe3ZuLe3t87+GhsbMXnyZAwfPhzl5eXIyMjAH/7wB6Snp2u3uXr1Kuzs7HD//n3s3bsX8fHxuHXrVqv1Xbp0Ca+99prOspiYGCxduhRKpRK3b9/GrFmzdNYPHTrUKq9Fd2UMVDKKTCbDtGnT0L17d9jZ2SExMRE3btxAeXk5gBczCdy9exfl5eWQSqUIDg7W+f6xY8cwdepUpKWlITo62uDje3l5GTTwb1NTE9atWwcHBwc4ODgAAObMmQMnJydIJBIsXboUTU1N+O9//6vX/s6dO4cHDx7gd7/7Hezs7DBs2DDExsbqjE/r6uqKpUuXQiKRIDQ0FP369WszAJ88eaId1LiZRCLBzZs3UVlZie7du7cYeFsmk+HJkyd6/w2o4zFQySh1dXVYuHAh+vbtC5lMhr59+wKAtud527ZteP78OUaMGIHXXnutxU/6lJQUjBs3DmFhYUYd//79+wYN/Ovo6KhzWaCpqQkrVqzAwIEDIZPJIJfLUV1drXfPeVlZGXr16gVbW1vtMl9fX5SVlWk/e3p66nyne/fuUCqVre7v5UGNm2VkZKCwsBCDBw+Gv78/9u3bp7O+pqYGzs7OetVLnYOBSkZZv349CgoKcPr0adTU1ODOnTsAoP3J379/f6SlpeHhw4f4+uuvER8fj4KCAu3309PTce3aNcTGxsKYEST379+P0NBQAC/uRKirq9PZz4MHD3S2F4t1/1VPT09Heno6/vnPf6K6uhoKhQJOTk7affxw+x/q3bs3ysvL0dDQoF1WUlKC3r17G9wWAHjzzTdx/fp1nWU//elPsX//flRVVeG3v/0tZsyYgYqKCu36oqKiFncikHkxUMkoNTU1sLOzg7OzM2pra7F8+XKd9WlpaaioqIBIJIKzszPEYrHOZIIuLi44ceIEzp49i4ULF+odqnfv3sUnn3yCvLw8bafToEGDIJFIkJ6eDrVajT179uDixYuvrN/W1hZubm6or6/H6tWrdc4QPTw88ODBA9TV1bX6/ZEjR8LDwwMrV66ESqVCYWEhNm/ejJiYGL3a0dr+gBfXXQGgvr4eO3fuxJMnTyAWi7Vn181/w7t376KqqqrFpRQyLwYqGSUuLg7dunWDh4cH/Pz8tD3nzbKysjB8+HD06NEDU6ZMweeff47hw4frbOPs7IysrCxcuHABc+fObTNUv/zySzg6OkImkyE8PBxPnz7FhQsXMGTIEAAvriX+9a9/xW9+8xu4urri9OnTmDBhQrv1x8TE4PXXX4ePjw98fX1hb2+PPn36aNeHhYVh9OjR8PLyglwuR2lpqc73JRIJDh8+jIKCAnh6emLKlCmIi4vDjBkz9P4bvszGxgbz5s1Damqqdll6ejoGDBgAR0dHfPzxx0hPT4erqyuAF//Dmj17Nrp3727U8ahjcMR+IgtRU1MDf39/nD17tt2b+5vvRMjLy7O6OcW6OgYqEZFA+JOfiEggDFQiIoEwUImIBMJAJSISCAOViEggDFQiIoEwUImIBMJAJSISCAOViEggDFQiIoEwUImIBPJ/YJj6ehs1gegAAAAASUVORK5CYII=", "text/plain": [ "
" ] @@ -160,7 +170,7 @@ "source": [ "fig, ax = plt.subplots(figsize=(3.5, 1.75))\n", "\n", - "for (gid, group), m in zip(results.groupby(['task_input_size', 'parallel_tasks']), ['o', 's', '^', 'v']):\n", + "for (gid, group), m in zip(results.query('host==\"x3002c0s37b0n0\"').groupby(['task_input_size', 'parallel_tasks']), ['o', 's', '^', 'v']):\n", " group.sort_values(['task_length', 'utilization'], ascending=False, inplace=True)\n", " group.drop_duplicates('task_length', inplace=True, keep='first')\n", " ax.semilogx(group['task_length'], group['utilization'] * 100, '--'+m, label=f'$s$={gid[0]}MB, $N$={gid[1]}')\n", @@ -174,181 +184,165 @@ "fig.savefig('performance-envelope.pdf')" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Measure the Response, Decision, and Dispatch Times\n", + "We can measure three sources of latency for applications using the logs in the `Result` object for response and dispatch, and the Colmena logs for the reaction time" + ] + }, { "cell_type": "code", "execution_count": 8, "metadata": {}, + "outputs": [], + "source": [ + "def get_median_reaction_time(path: Path):\n", + " \"\"\"Measure the median reaction time for all tasks, total and data-related, \n", + " broken down by compute and data transit\"\"\"\n", + "\n", + " # Loop over the tasks\n", + " compute = []\n", + " data = []\n", + " with path.joinpath('results.json').open() as fp:\n", + " for line in fp:\n", + " record = json.loads(line)\n", + " compute_time = (\n", + " record['timestamp']['result_received'] -\n", + " record['timestamp']['compute_ended']\n", + " ) # Time for the compute message to arrive\n", + " compute.append(compute_time)\n", + "\n", + " # Additional time to read the data\n", + " data_time = compute_time + record['task_info']['read_time']\n", + " data.append(data_time)\n", + "\n", + " return np.percentile(compute, 50), np.percentile(data, 50)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "results['path'].apply(get_median_reaction_time)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, "outputs": [ { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
configlocal_hosttask_input_sizetask_output_sizetask_countworker_counttask_lengthtask_length_stduse_proxystoreproxystore_thresholdreuse_dataoutput_dirstore_configparsl_configparallel_taskspathutilization
0NoneTrue1.01.01281610.00.01True1FalserunsStore(name=store, connector=RedisConnector(hos...Config(\\n app_cache=True, \\n checkpoint_...16runs/bettik-linux-2024-05-16_13-39-140.997314
3NoneTrue100.0100.01281610.00.01True1FalserunsStore(name=store, connector=RedisConnector(hos...Config(\\n app_cache=True, \\n checkpoint_...16runs/bettik-linux-2024-05-16_13-48-090.452690
6NoneTrue0.10.11281610.00.01True1FalserunsStore(name=store, connector=RedisConnector(hos...Config(\\n app_cache=True, \\n checkpoint_...16runs/bettik-linux-2024-05-16_13-37-170.997465
10NoneTrue10.010.01281610.00.01True1FalserunsStore(name=store, connector=RedisConnector(hos...Config(\\n app_cache=True, \\n checkpoint_...16runs/bettik-linux-2024-05-16_13-41-260.976018
\n", - "
" - ], - "text/plain": [ - " config local_host task_input_size task_output_size task_count \\\n", - "0 None True 1.0 1.0 128 \n", - "3 None True 100.0 100.0 128 \n", - "6 None True 0.1 0.1 128 \n", - "10 None True 10.0 10.0 128 \n", - "\n", - " worker_count task_length task_length_std use_proxystore \\\n", - "0 16 10.0 0.01 True \n", - "3 16 10.0 0.01 True \n", - "6 16 10.0 0.01 True \n", - "10 16 10.0 0.01 True \n", - "\n", - " proxystore_threshold reuse_data output_dir \\\n", - "0 1 False runs \n", - "3 1 False runs \n", - "6 1 False runs \n", - "10 1 False runs \n", - "\n", - " store_config \\\n", - "0 Store(name=store, connector=RedisConnector(hos... \n", - "3 Store(name=store, connector=RedisConnector(hos... \n", - "6 Store(name=store, connector=RedisConnector(hos... \n", - "10 Store(name=store, connector=RedisConnector(hos... \n", - "\n", - " parsl_config parallel_tasks \\\n", - "0 Config(\\n app_cache=True, \\n checkpoint_... 16 \n", - "3 Config(\\n app_cache=True, \\n checkpoint_... 16 \n", - "6 Config(\\n app_cache=True, \\n checkpoint_... 16 \n", - "10 Config(\\n app_cache=True, \\n checkpoint_... 16 \n", - "\n", - " path utilization \n", - "0 runs/bettik-linux-2024-05-16_13-39-14 0.997314 \n", - "3 runs/bettik-linux-2024-05-16_13-48-09 0.452690 \n", - "6 runs/bettik-linux-2024-05-16_13-37-17 0.997465 \n", - "10 runs/bettik-linux-2024-05-16_13-41-26 0.976018 " - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" + "ename": "IndexError", + "evalue": "cannot do a non-empty take from an empty axes.", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mIndexError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[9], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m results[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mrxn_time_compute\u001b[39m\u001b[38;5;124m'\u001b[39m], results[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mrxn_time_data\u001b[39m\u001b[38;5;124m'\u001b[39m] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mzip\u001b[39m(\u001b[38;5;241m*\u001b[39mresults[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mpath\u001b[39m\u001b[38;5;124m'\u001b[39m]\u001b[38;5;241m.\u001b[39mapply(get_median_reaction_time))\n", + "File \u001b[0;32m~/miniconda3/envs/colmena/lib/python3.11/site-packages/pandas/core/series.py:4771\u001b[0m, in \u001b[0;36mSeries.apply\u001b[0;34m(self, func, convert_dtype, args, **kwargs)\u001b[0m\n\u001b[1;32m 4661\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mapply\u001b[39m(\n\u001b[1;32m 4662\u001b[0m \u001b[38;5;28mself\u001b[39m,\n\u001b[1;32m 4663\u001b[0m func: AggFuncType,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 4666\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs,\n\u001b[1;32m 4667\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m DataFrame \u001b[38;5;241m|\u001b[39m Series:\n\u001b[1;32m 4668\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 4669\u001b[0m \u001b[38;5;124;03m Invoke function on values of Series.\u001b[39;00m\n\u001b[1;32m 4670\u001b[0m \n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 4769\u001b[0m \u001b[38;5;124;03m dtype: float64\u001b[39;00m\n\u001b[1;32m 4770\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m-> 4771\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m SeriesApply(\u001b[38;5;28mself\u001b[39m, func, convert_dtype, args, kwargs)\u001b[38;5;241m.\u001b[39mapply()\n", + "File \u001b[0;32m~/miniconda3/envs/colmena/lib/python3.11/site-packages/pandas/core/apply.py:1123\u001b[0m, in \u001b[0;36mSeriesApply.apply\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 1120\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mapply_str()\n\u001b[1;32m 1122\u001b[0m \u001b[38;5;66;03m# self.f is Callable\u001b[39;00m\n\u001b[0;32m-> 1123\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mapply_standard()\n", + "File \u001b[0;32m~/miniconda3/envs/colmena/lib/python3.11/site-packages/pandas/core/apply.py:1174\u001b[0m, in \u001b[0;36mSeriesApply.apply_standard\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 1172\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 1173\u001b[0m values \u001b[38;5;241m=\u001b[39m obj\u001b[38;5;241m.\u001b[39mastype(\u001b[38;5;28mobject\u001b[39m)\u001b[38;5;241m.\u001b[39m_values\n\u001b[0;32m-> 1174\u001b[0m mapped \u001b[38;5;241m=\u001b[39m lib\u001b[38;5;241m.\u001b[39mmap_infer(\n\u001b[1;32m 1175\u001b[0m values,\n\u001b[1;32m 1176\u001b[0m f,\n\u001b[1;32m 1177\u001b[0m convert\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mconvert_dtype,\n\u001b[1;32m 1178\u001b[0m )\n\u001b[1;32m 1180\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(mapped) \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(mapped[\u001b[38;5;241m0\u001b[39m], ABCSeries):\n\u001b[1;32m 1181\u001b[0m \u001b[38;5;66;03m# GH#43986 Need to do list(mapped) in order to get treated as nested\u001b[39;00m\n\u001b[1;32m 1182\u001b[0m \u001b[38;5;66;03m# See also GH#25959 regarding EA support\u001b[39;00m\n\u001b[1;32m 1183\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m obj\u001b[38;5;241m.\u001b[39m_constructor_expanddim(\u001b[38;5;28mlist\u001b[39m(mapped), index\u001b[38;5;241m=\u001b[39mobj\u001b[38;5;241m.\u001b[39mindex)\n", + "File \u001b[0;32m~/miniconda3/envs/colmena/lib/python3.11/site-packages/pandas/_libs/lib.pyx:2924\u001b[0m, in \u001b[0;36mpandas._libs.lib.map_infer\u001b[0;34m()\u001b[0m\n", + "Cell \u001b[0;32mIn[8], line 21\u001b[0m, in \u001b[0;36mget_median_reaction_time\u001b[0;34m(path)\u001b[0m\n\u001b[1;32m 18\u001b[0m data_time \u001b[38;5;241m=\u001b[39m compute_time \u001b[38;5;241m+\u001b[39m record[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mtask_info\u001b[39m\u001b[38;5;124m'\u001b[39m][\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mread_time\u001b[39m\u001b[38;5;124m'\u001b[39m]\n\u001b[1;32m 19\u001b[0m data\u001b[38;5;241m.\u001b[39mappend(data_time)\n\u001b[0;32m---> 21\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m np\u001b[38;5;241m.\u001b[39mpercentile(compute, \u001b[38;5;241m50\u001b[39m), np\u001b[38;5;241m.\u001b[39mpercentile(data, \u001b[38;5;241m50\u001b[39m)\n", + "File \u001b[0;32m<__array_function__ internals>:200\u001b[0m, in \u001b[0;36mpercentile\u001b[0;34m(*args, **kwargs)\u001b[0m\n", + "File \u001b[0;32m~/miniconda3/envs/colmena/lib/python3.11/site-packages/numpy/lib/function_base.py:4205\u001b[0m, in \u001b[0;36mpercentile\u001b[0;34m(a, q, axis, out, overwrite_input, method, keepdims, interpolation)\u001b[0m\n\u001b[1;32m 4203\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m _quantile_is_valid(q):\n\u001b[1;32m 4204\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mPercentiles must be in the range [0, 100]\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m-> 4205\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m _quantile_unchecked(\n\u001b[1;32m 4206\u001b[0m a, q, axis, out, overwrite_input, method, keepdims)\n", + "File \u001b[0;32m~/miniconda3/envs/colmena/lib/python3.11/site-packages/numpy/lib/function_base.py:4473\u001b[0m, in \u001b[0;36m_quantile_unchecked\u001b[0;34m(a, q, axis, out, overwrite_input, method, keepdims)\u001b[0m\n\u001b[1;32m 4465\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m_quantile_unchecked\u001b[39m(a,\n\u001b[1;32m 4466\u001b[0m q,\n\u001b[1;32m 4467\u001b[0m axis\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 4470\u001b[0m method\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mlinear\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m 4471\u001b[0m keepdims\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mFalse\u001b[39;00m):\n\u001b[1;32m 4472\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Assumes that q is in [0, 1], and is an ndarray\"\"\"\u001b[39;00m\n\u001b[0;32m-> 4473\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m _ureduce(a,\n\u001b[1;32m 4474\u001b[0m func\u001b[38;5;241m=\u001b[39m_quantile_ureduce_func,\n\u001b[1;32m 4475\u001b[0m q\u001b[38;5;241m=\u001b[39mq,\n\u001b[1;32m 4476\u001b[0m keepdims\u001b[38;5;241m=\u001b[39mkeepdims,\n\u001b[1;32m 4477\u001b[0m axis\u001b[38;5;241m=\u001b[39maxis,\n\u001b[1;32m 4478\u001b[0m out\u001b[38;5;241m=\u001b[39mout,\n\u001b[1;32m 4479\u001b[0m overwrite_input\u001b[38;5;241m=\u001b[39moverwrite_input,\n\u001b[1;32m 4480\u001b[0m method\u001b[38;5;241m=\u001b[39mmethod)\n", + "File \u001b[0;32m~/miniconda3/envs/colmena/lib/python3.11/site-packages/numpy/lib/function_base.py:3752\u001b[0m, in \u001b[0;36m_ureduce\u001b[0;34m(a, func, keepdims, **kwargs)\u001b[0m\n\u001b[1;32m 3749\u001b[0m index_out \u001b[38;5;241m=\u001b[39m (\u001b[38;5;241m0\u001b[39m, ) \u001b[38;5;241m*\u001b[39m nd\n\u001b[1;32m 3750\u001b[0m kwargs[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mout\u001b[39m\u001b[38;5;124m'\u001b[39m] \u001b[38;5;241m=\u001b[39m out[(\u001b[38;5;28mEllipsis\u001b[39m, ) \u001b[38;5;241m+\u001b[39m index_out]\n\u001b[0;32m-> 3752\u001b[0m r \u001b[38;5;241m=\u001b[39m func(a, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n\u001b[1;32m 3754\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m out \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m 3755\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m out\n", + "File \u001b[0;32m~/miniconda3/envs/colmena/lib/python3.11/site-packages/numpy/lib/function_base.py:4639\u001b[0m, in \u001b[0;36m_quantile_ureduce_func\u001b[0;34m(a, q, axis, out, overwrite_input, method)\u001b[0m\n\u001b[1;32m 4637\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 4638\u001b[0m arr \u001b[38;5;241m=\u001b[39m a\u001b[38;5;241m.\u001b[39mcopy()\n\u001b[0;32m-> 4639\u001b[0m result \u001b[38;5;241m=\u001b[39m _quantile(arr,\n\u001b[1;32m 4640\u001b[0m quantiles\u001b[38;5;241m=\u001b[39mq,\n\u001b[1;32m 4641\u001b[0m axis\u001b[38;5;241m=\u001b[39maxis,\n\u001b[1;32m 4642\u001b[0m method\u001b[38;5;241m=\u001b[39mmethod,\n\u001b[1;32m 4643\u001b[0m out\u001b[38;5;241m=\u001b[39mout)\n\u001b[1;32m 4644\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m result\n", + "File \u001b[0;32m~/miniconda3/envs/colmena/lib/python3.11/site-packages/numpy/lib/function_base.py:4745\u001b[0m, in \u001b[0;36m_quantile\u001b[0;34m(arr, quantiles, axis, method, out)\u001b[0m\n\u001b[1;32m 4737\u001b[0m arr\u001b[38;5;241m.\u001b[39mpartition(\n\u001b[1;32m 4738\u001b[0m np\u001b[38;5;241m.\u001b[39munique(np\u001b[38;5;241m.\u001b[39mconcatenate(([\u001b[38;5;241m0\u001b[39m, \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m1\u001b[39m],\n\u001b[1;32m 4739\u001b[0m previous_indexes\u001b[38;5;241m.\u001b[39mravel(),\n\u001b[1;32m 4740\u001b[0m next_indexes\u001b[38;5;241m.\u001b[39mravel(),\n\u001b[1;32m 4741\u001b[0m ))),\n\u001b[1;32m 4742\u001b[0m axis\u001b[38;5;241m=\u001b[39mDATA_AXIS)\n\u001b[1;32m 4743\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m np\u001b[38;5;241m.\u001b[39missubdtype(arr\u001b[38;5;241m.\u001b[39mdtype, np\u001b[38;5;241m.\u001b[39minexact):\n\u001b[1;32m 4744\u001b[0m slices_having_nans \u001b[38;5;241m=\u001b[39m np\u001b[38;5;241m.\u001b[39misnan(\n\u001b[0;32m-> 4745\u001b[0m take(arr, indices\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m-\u001b[39m\u001b[38;5;241m1\u001b[39m, axis\u001b[38;5;241m=\u001b[39mDATA_AXIS)\n\u001b[1;32m 4746\u001b[0m )\n\u001b[1;32m 4747\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 4748\u001b[0m slices_having_nans \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m\n", + "File \u001b[0;32m<__array_function__ internals>:200\u001b[0m, in \u001b[0;36mtake\u001b[0;34m(*args, **kwargs)\u001b[0m\n", + "File \u001b[0;32m~/miniconda3/envs/colmena/lib/python3.11/site-packages/numpy/core/fromnumeric.py:190\u001b[0m, in \u001b[0;36mtake\u001b[0;34m(a, indices, axis, out, mode)\u001b[0m\n\u001b[1;32m 93\u001b[0m \u001b[38;5;129m@array_function_dispatch\u001b[39m(_take_dispatcher)\n\u001b[1;32m 94\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mtake\u001b[39m(a, indices, axis\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m, out\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m, mode\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mraise\u001b[39m\u001b[38;5;124m'\u001b[39m):\n\u001b[1;32m 95\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 96\u001b[0m \u001b[38;5;124;03m Take elements from an array along an axis.\u001b[39;00m\n\u001b[1;32m 97\u001b[0m \n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 188\u001b[0m \u001b[38;5;124;03m [5, 7]])\u001b[39;00m\n\u001b[1;32m 189\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m--> 190\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m _wrapfunc(a, \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mtake\u001b[39m\u001b[38;5;124m'\u001b[39m, indices, axis\u001b[38;5;241m=\u001b[39maxis, out\u001b[38;5;241m=\u001b[39mout, mode\u001b[38;5;241m=\u001b[39mmode)\n", + "File \u001b[0;32m~/miniconda3/envs/colmena/lib/python3.11/site-packages/numpy/core/fromnumeric.py:57\u001b[0m, in \u001b[0;36m_wrapfunc\u001b[0;34m(obj, method, *args, **kwds)\u001b[0m\n\u001b[1;32m 54\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m _wrapit(obj, method, \u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwds)\n\u001b[1;32m 56\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m---> 57\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m bound(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwds)\n\u001b[1;32m 58\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m:\n\u001b[1;32m 59\u001b[0m \u001b[38;5;66;03m# A TypeError occurs if the object does have such a method in its\u001b[39;00m\n\u001b[1;32m 60\u001b[0m \u001b[38;5;66;03m# class, but its signature is not identical to that of NumPy's. This\u001b[39;00m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 64\u001b[0m \u001b[38;5;66;03m# Call _wrapit from within the except clause to ensure a potential\u001b[39;00m\n\u001b[1;32m 65\u001b[0m \u001b[38;5;66;03m# exception has a traceback chain.\u001b[39;00m\n\u001b[1;32m 66\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m _wrapit(obj, method, \u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwds)\n", + "\u001b[0;31mIndexError\u001b[0m: cannot do a non-empty take from an empty axes." + ] } ], "source": [ - "results.query('task_length > 5')" + "results['rxn_time_compute'], results['rxn_time_data'] = zip(*results['path'].apply(get_median_reaction_time))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def get_median_decision_time(path: Path):\n", + " \"\"\"Measure the median time for all job submissions.\"\"\"\n", + "\n", + " decision_time = []\n", + " pat = re.compile('Finished submitting new work. Runtime: (\\d\\.\\d+e-?\\d+)s')\n", + " with path.joinpath('run.log').open() as fp:\n", + " for line in fp:\n", + " for match in pat.findall(line):\n", + " decision_time.append(float(match))\n", + " return np.percentile(decision_time, 50) " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "results['decision_time'] = results['path'].apply(get_median_decision_time)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def get_median_dispatch_time(path: Path):\n", + " \"\"\"Measure the median dispatch time for all tasks,\n", + " by until the compute message arrives and when the data arrives\"\"\"\n", + "\n", + " # Loop over the tasks\n", + " compute = []\n", + " data = []\n", + " with path.joinpath('results.json').open() as fp:\n", + " for line in fp:\n", + " record = json.loads(line)\n", + " compute_time = (\n", + " record['timestamp']['compute_started'] \n", + " - record['timestamp']['created']\n", + " + record['time']['deserialize_inputs']\n", + " )\n", + " compute.append(compute_time)\n", + "\n", + " # Add the additional time taken for the data to be accessed\n", + " data_time = 0\n", + " for proxy, timings in record['time'].get('proxy', {}).items():\n", + " if 'store.get' in timings['times']:\n", + " data_time += timings['times']['store.get']['avg_time_ms'] / 1000\n", + " data.append(\n", + " compute_time + data_time\n", + " )\n", + "\n", + " return np.percentile(compute, 50), np.percentile(data, 50)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "results['dispatch_time_compute'], results['dispatch_time_data'] = zip(*results['path'].apply(get_median_dispatch_time))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "results" ] }, { diff --git a/demo_apps/task-limits/run.py b/demo_apps/task-limits/run.py index cd692c0..17369e0 100644 --- a/demo_apps/task-limits/run.py +++ b/demo_apps/task-limits/run.py @@ -1,7 +1,7 @@ """Evaluate the effect of task duration and size on throughput""" from platform import node from datetime import datetime -from random import randbytes +from time import perf_counter from typing import TextIO import argparse import json @@ -11,7 +11,6 @@ import time import numpy as np -from proxystore.connectors.file import FileConnector from proxystore.connectors.redis import RedisConnector from proxystore.store import Store, register_store from scipy.stats import truncnorm @@ -118,7 +117,7 @@ def __init__(self, def submit(self): """Submit a new task if resources are available""" runtime, task_size = self.task_queue.pop() - input_data = randbytes(task_size) + input_data = np.empty(task_size, bool) self.queues.send_inputs( input_data, self.task_output_size, runtime, method='target_function') @@ -129,6 +128,15 @@ def submit(self): def resubmitter(self, result: Result): assert result.success, result.failure_info.traceback self.rec.release() + + # Force access to the data + read_time = perf_counter() + data_size = len(result.value) + read_time = perf_counter() - read_time + result.task_info['read_time'] = read_time + result.task_info['read_size'] = data_size + + # Store print(result.json(exclude={'inputs', 'value'}), file=self.output_file, flush=False)